Repository: sub-store-org/Sub-Store Branch: master Commit: be19860cd0f7 Files: 115 Total size: 911.4 KB Directory structure: gitextract_j4ptg362/ ├── .github/ │ └── workflows/ │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend/ │ ├── .babelrc │ ├── .eslintrc.json │ ├── .prettierrc.json │ ├── banner │ ├── bundle-esbuild.js │ ├── bundle.js │ ├── dev-esbuild.js │ ├── dist/ │ │ └── .gitkeep │ ├── gulpfile.babel.js │ ├── jsconfig.json │ ├── package.json │ ├── patches/ │ │ └── http-proxy@1.18.1.patch │ └── src/ │ ├── constants.js │ ├── core/ │ │ ├── app.js │ │ ├── proxy-utils/ │ │ │ ├── index.js │ │ │ ├── parsers/ │ │ │ │ ├── index.js │ │ │ │ └── peggy/ │ │ │ │ ├── loon.js │ │ │ │ ├── loon.peg │ │ │ │ ├── qx.js │ │ │ │ ├── qx.peg │ │ │ │ ├── surge.js │ │ │ │ ├── surge.peg │ │ │ │ ├── trojan-uri.js │ │ │ │ └── trojan-uri.peg │ │ │ ├── preprocessors/ │ │ │ │ └── index.js │ │ │ ├── processors/ │ │ │ │ └── index.js │ │ │ ├── producers/ │ │ │ │ ├── clash.js │ │ │ │ ├── clashmeta.js │ │ │ │ ├── egern.js │ │ │ │ ├── index.js │ │ │ │ ├── loon.js │ │ │ │ ├── qx.js │ │ │ │ ├── shadowrocket.js │ │ │ │ ├── sing-box.js │ │ │ │ ├── stash.js │ │ │ │ ├── surfboard.js │ │ │ │ ├── surge.js │ │ │ │ ├── surgemac.js │ │ │ │ ├── uri.js │ │ │ │ ├── utils.js │ │ │ │ └── v2ray.js │ │ │ └── validators/ │ │ │ └── index.js │ │ └── rule-utils/ │ │ ├── index.js │ │ ├── parsers.js │ │ ├── preprocessors.js │ │ └── producers.js │ ├── main.js │ ├── products/ │ │ ├── cron-sync-artifacts.js │ │ ├── resource-parser.loon.js │ │ ├── sub-store-0.js │ │ └── sub-store-1.js │ ├── restful/ │ │ ├── artifacts.js │ │ ├── collections.js │ │ ├── download.js │ │ ├── errors/ │ │ │ └── index.js │ │ ├── file.js │ │ ├── index.js │ │ ├── miscs.js │ │ ├── module.js │ │ ├── node-info.js │ │ ├── parser.js │ │ ├── preview.js │ │ ├── response.js │ │ ├── settings.js │ │ ├── sort.js │ │ ├── subscriptions.js │ │ ├── sync.js │ │ └── token.js │ ├── test/ │ │ └── proxy-parsers/ │ │ ├── loon.spec.js │ │ ├── qx.spec.js │ │ ├── surge.spec.js │ │ └── testcases.js │ ├── utils/ │ │ ├── database.js │ │ ├── dns.js │ │ ├── download.js │ │ ├── env.js │ │ ├── flow.js │ │ ├── geo.js │ │ ├── gist.js │ │ ├── headers-resource-cache.js │ │ ├── index.js │ │ ├── logical.js │ │ ├── migration.js │ │ ├── resource-cache.js │ │ ├── rs.js │ │ ├── script-resource-cache.js │ │ ├── user-agent.js │ │ └── yaml.js │ └── vendor/ │ ├── express.js │ ├── md5.js │ └── open-api.js ├── config/ │ ├── Egern.yaml │ ├── Loon.plugin │ ├── QX-Task.json │ ├── QX.snippet │ ├── README.md │ ├── Stash.stoverride │ ├── Surge-Beta.sgmodule │ ├── Surge-Noability.sgmodule │ ├── Surge-ability.sgmodule │ └── Surge.sgmodule ├── scripts/ │ ├── demo.js │ ├── fancy-characters.js │ ├── ip-flag-node.js │ ├── ip-flag.js │ ├── media-filter.js │ ├── revert.js │ ├── tls-fingerprint.js │ ├── udp-filter.js │ └── vmess-ws-obfs-host.js └── vs.code-workspace ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/main.yml ================================================ name: build on: workflow_dispatch: push: branches: - master paths: - "backend/package.json" pull_request: branches: - master paths: - "backend/package.json" jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: ref: "master" - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: "20" - name: Install dependencies run: | npm install -g pnpm cd backend && pnpm i --no-frozen-lockfile # - name: Test # run: | # cd backend # pnpm test # - name: Build # run: | # cd backend # pnpm run build - name: Bundle run: | cd backend pnpm bundle:esbuild - id: tag name: Generate release tag run: | cd backend SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"` echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT - name: Prepare release run: | cd backend pnpm i -D conventional-changelog-cli pnpm run changelog - name: Release uses: softprops/action-gh-release@v1 if: ${{ success() }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: body_path: ./backend/CHANGELOG.md tag_name: ${{ steps.tag.outputs.release_tag }} # generate_release_notes: true files: | ./backend/sub-store.min.js ./backend/dist/sub-store-0.min.js ./backend/dist/sub-store-1.min.js ./backend/dist/sub-store-parser.loon.min.js ./backend/dist/cron-sync-artifacts.min.js ./backend/dist/sub-store.bundle.js - name: Git push assets to "release" branch run: | cd backend/dist || exit 1 git init git config --local user.name "github-actions[bot]" git config --local user.email "github-actions[bot]@users.noreply.github.com" git checkout -b release git add . git commit -m "release: ${{ steps.tag.outputs.release_tag }}" git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}" git push -f -u origin release # - name: Sync to GitLab # env: # GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }} # run: | # curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline ================================================ FILE: .gitignore ================================================ .DS_Store # json config sub-store.json sub-store_*.json root.json # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt # dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? # Dist files backend/dist/* !backend/dist/.gitkeep backend/sub-store.min.js CHANGELOG.md .codeartsdoer .github/copilot-instructions.md ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (c) 2015 Ayuntamiento de Madrid Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================

Sub-Store

Sub-Store

Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.

[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store) sub-store-org%2FSub-Store | Trendshift [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM) [📚 文档/DOC](https://github.com/sub-store-org/Sub-Store/wiki) Core functionalities: 1. Conversion among various formats. 2. Subscription formatting. 3. Collect multiple subscriptions in one URL. > The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information. ## 1. Subscription Conversion ### Supported Input Formats [本地节点怎么写/How To Write A Local Node](https://t.me/zhetengsha/824) > ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs. However, we have already supported some very common non-standard URIs (such as VMess, VLESS). - [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok)) example: `socks5+tls://user:pass@ip:port#name` - [x] URI(AnyTLS, SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard) > Please note, HTTP(s) does not have a standard URI format, so it is not supported. Please use other formats. - [x] Clash Proxies YAML - [x] Clash Proxy JSON/JSON5/YAML(single line) > [NaiveProxy](https://t.me/zhetengsha/4308) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2, AnyTLS) - [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, AnyTLS, TrustTunnel, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge)) - [x] mihomo(Clash.Meta) Compatible (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, sudoku, AnyTLS, MASQUE) Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`): - [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard) ### Supported Target Platforms - [x] Plain JSON - [x] Stash - [x] Clash.Meta(mihomo) - [x] Surfboard - [x] Surge - [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself) - [x] Loon - [x] Egern - [x] Shadowrocket - [x] QX - [x] sing-box - [x] V2Ray - [x] V2Ray URI Deprecated: - [x] Clash ## 2. Subscription Formatting ### Filtering - [x] **Regex filter** - [x] **Discard regex filter** - [x] **Region filter** - [x] **Type filter** - [x] **Useless proxies filter** - [x] **Script filter** ### Proxy Operations - [x] **Set property operator**: set some proxy properties such as `udp`,`tfo`, `skip-cert-verify` etc. - [x] **Flag operator**: add flags or remove flags for proxies. - [x] **Sort operator**: sort proxies by name. - [x] **Regex sort operator**: sort proxies by keywords (fallback to normal sort). - [x] **Regex rename operator**: replace by regex in proxy names. - [x] **Regex delete operator**: delete by regex in proxy names. - [x] **Script operator**: modify proxy by script. - [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address. ### Development Install `pnpm` Go to `backend` directories, install node dependencies: ``` pnpm i ``` ``` SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/" ``` ### Build ``` pnpm bundle:esbuild ``` ## LICENSE This project is under the GPL V3 LICENSE. [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large) ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date) ## Acknowledgements - Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work! - Special thanks to @Orz-3 and @58xinian for their awesome icons. ## Sponsors [![image](./support.nodeseek.com_page_promotion_id=8.png)](https://yxvm.com) [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) sponsored this project. ================================================ FILE: backend/.babelrc ================================================ { "presets": [ [ "@babel/preset-env" ] ], "env": { "test": { "presets": [ "@babel/preset-env" ] } }, "plugins": [ [ "babel-plugin-relative-path-import", { "paths": [ { "rootPathPrefix": "@", "rootPathSuffix": "src" } ] } ] ] } ================================================ FILE: backend/.eslintrc.json ================================================ { "ignorePatterns": ["*.min.js", "src/vendor/*.js"], "env": { "browser": true, "es2021": true, "node": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "rules": { } } ================================================ FILE: backend/.prettierrc.json ================================================ { "singleQuote": true, "trailingComma": "all", "tabWidth": 4, "bracketSpacing": true } ================================================ FILE: backend/banner ================================================ /** * ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗ * ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ * ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗ * ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝ * ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket! * @updated: <%= updated %> * @version: <%= pkg.version %> * @author: Peng-YM * @github: https://github.com/sub-store-org/Sub-Store * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 */ ================================================ FILE: backend/bundle-esbuild.js ================================================ #!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { build } = require('esbuild'); !(async () => { const version = JSON.parse( fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'), ).version.trim(); const artifacts = [ { src: 'src/main.js', dest: 'sub-store.min.js' }, { src: 'src/products/resource-parser.loon.js', dest: 'dist/sub-store-parser.loon.min.js', }, { src: 'src/products/cron-sync-artifacts.js', dest: 'dist/cron-sync-artifacts.min.js', }, { src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' }, { src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' }, ]; for await (const artifact of artifacts) { await build({ entryPoints: [artifact.src], bundle: true, minify: true, sourcemap: false, platform: 'browser', format: 'iife', outfile: artifact.dest, }); } let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), { encoding: 'utf8', }); content = content.replace( /eval\(('|")(require\(('|").*?('|")\))('|")\)/g, '$2', ); fs.writeFileSync( path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, { encoding: 'utf8', }, ); await build({ entryPoints: ['dist/sub-store.no-bundle.js'], bundle: true, minify: true, sourcemap: false, platform: 'node', format: 'cjs', outfile: 'dist/sub-store.bundle.js', }); fs.writeFileSync( path.join(__dirname, 'dist/sub-store.bundle.js'), `// SUB_STORE_BACKEND_VERSION: ${version} ${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), { encoding: 'utf8', })}`, { encoding: 'utf8', }, ); })() .catch((e) => { console.log(e); }) .finally(() => { console.log('done'); }); ================================================ FILE: backend/bundle.js ================================================ #!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { build } = require('esbuild'); !(async () => { const version = JSON.parse( fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'), ).version.trim(); let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), { encoding: 'utf8', }); content = content.replace( /eval\(('|")(require\(('|").*?('|")\))('|")\)/g, '$2', ); fs.writeFileSync( path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, { encoding: 'utf8', }, ); await build({ entryPoints: ['dist/sub-store.no-bundle.js'], bundle: true, minify: true, sourcemap: true, platform: 'node', format: 'cjs', outfile: 'dist/sub-store.bundle.js', }); fs.writeFileSync( path.join(__dirname, 'dist/sub-store.bundle.js'), `// SUB_STORE_BACKEND_VERSION: ${version} ${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), { encoding: 'utf8', })}`, { encoding: 'utf8', }, ); })() .catch((e) => { console.log(e); }) .finally(() => { console.log('done'); }); ================================================ FILE: backend/dev-esbuild.js ================================================ #!/usr/bin/env node const { build } = require('esbuild'); !(async () => { const artifacts = [{ src: 'src/main.js', dest: 'sub-store.min.js' }]; for await (const artifact of artifacts) { await build({ entryPoints: [artifact.src], bundle: true, minify: false, sourcemap: false, platform: 'node', format: 'cjs', outfile: artifact.dest, }); } })() .catch((e) => { console.log(e); }) .finally(() => { console.log('done'); }); ================================================ FILE: backend/dist/.gitkeep ================================================ ================================================ FILE: backend/gulpfile.babel.js ================================================ import fs from 'fs'; import browserify from 'browserify'; import gulp from 'gulp'; import prettier from 'gulp-prettier'; import header from 'gulp-header'; import eslint from 'gulp-eslint-new'; import newFile from 'gulp-file'; import path from 'path'; import tap from 'gulp-tap'; import pkg from './package.json'; export function peggy() { return gulp.src('src/**/*.peg').pipe( tap(function (file) { const filename = path.basename(file.path).split('.')[0] + '.js'; const raw = fs.readFileSync(file.path, 'utf8'); const contents = `import * as peggy from 'peggy'; const grammars = String.raw\`\n${raw}\n\`; let parser; export default function getParser() { if (!parser) { parser = peggy.generate(grammars); } return parser; }\n`; return newFile(filename, contents).pipe( gulp.dest(path.dirname(file.path)), ); }), ); } export function lint() { return gulp .src('src/**/*.js') .pipe(eslint({ fix: true })) .pipe(eslint.fix()) .pipe(eslint.format()) .pipe(eslint.failAfterError()); } export function styles() { return gulp .src('src/**/*.js') .pipe( prettier({ singleQuote: true, trailingComma: 'all', tabWidth: 4, bracketSpacing: true, }), ) .pipe(gulp.dest((file) => file.base)); } function scripts(src, dest) { return () => { return browserify(src) .transform('babelify', { presets: [['@babel/preset-env']], plugins: [ [ 'babel-plugin-relative-path-import', { paths: [ { rootPathPrefix: '@', rootPathSuffix: 'src', }, ], }, ], ], }) .plugin('tinyify') .bundle() .pipe(fs.createWriteStream(dest)); }; } function banner(dest) { return () => gulp .src(dest) .pipe( header(fs.readFileSync('./banner', 'utf-8'), { pkg, updated: new Date().toLocaleString('zh-CN'), }), ) .pipe(gulp.dest((file) => file.base)); } const artifacts = [ { src: 'src/main.js', dest: 'sub-store.min.js' }, { src: 'src/products/resource-parser.loon.js', dest: 'dist/sub-store-parser.loon.min.js', }, { src: 'src/products/cron-sync-artifacts.js', dest: 'dist/cron-sync-artifacts.min.js', }, { src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' }, { src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' }, ]; export const build = gulp.series( gulp.parallel( artifacts.map((artifact) => scripts(artifact.src, artifact.dest)), ), gulp.parallel(artifacts.map((artifact) => banner(artifact.dest))), ); const all = gulp.series(peggy, lint, styles, build); export default all; ================================================ FILE: backend/jsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } ================================================ FILE: backend/package.json ================================================ { "name": "sub-store", "version": "2.21.51", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.", "main": "src/main.js", "scripts": { "preinstall": "npx only-allow pnpm", "test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive", "serve": "node sub-store.min.js", "start": "nodemon -w src -w package.json --exec babel-node src/main.js", "dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js", "dev:run": "nodemon -w sub-store.min.js sub-store.min.js", "build": "gulp", "bundle": "node bundle.js", "bundle:esbuild": "node bundle-esbuild.js", "changelog": "conventional-changelog -p cli -i CHANGELOG.md -s" }, "author": "Peng-YM", "license": "GPL-3.0", "pnpm": { "patchedDependencies": { "http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch" } }, "dependencies": { "@maxmind/geoip2-node": "^5.0.0", "automerge": "1.0.1-preview.7", "body-parser": "^1.19.0", "buffer": "^6.0.3", "connect-history-api-fallback": "^2.0.0", "cron": "^3.1.6", "dns-packet": "^5.6.1", "dotenv": "^16.4.7", "express": "^4.17.1", "fastestsmallesttextencoderdecoder": "^1.0.22", "fetch-socks": "^1.3.2", "http-proxy-middleware": "^3.0.3", "ip-address": "^9.0.5", "js-base64": "^3.7.2", "json5": "^2.2.3", "jsrsasign": "^11.1.0", "lodash": "^4.17.21", "mime-types": "^2.1.35", "ms": "^2.1.3", "nanoid": "^3.3.3", "semver": "^7.6.3", "static-js-yaml": "^1.0.0", "undici": "^7.4.0" }, "devDependencies": { "@babel/core": "^7.18.0", "@babel/node": "^7.17.10", "@babel/preset-env": "^7.18.0", "@babel/register": "^7.17.7", "@types/gulp": "^4.0.9", "babel-plugin-relative-path-import": "^2.0.1", "babelify": "^10.0.0", "browser-pack-flat": "^3.4.2", "browserify": "^17.0.0", "chai": "^4.3.6", "esbuild": "^0.19.8", "eslint": "^8.16.0", "gulp": "^4.0.2", "gulp-babel": "^8.0.0", "gulp-eslint-new": "^1.4.4", "gulp-file": "^0.4.0", "gulp-header": "^2.0.9", "gulp-prettier": "^4.0.0", "gulp-tap": "^2.0.0", "mocha": "^10.0.0", "nodemon": "^2.0.16", "peggy": "^2.0.1", "prettier": "2.6.2", "prettier-plugin-sort-imports": "^1.6.1", "tinyify": "^3.0.0" } } ================================================ FILE: backend/patches/http-proxy@1.18.1.patch ================================================ diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644 --- a/lib/http-proxy/common.js +++ b/lib/http-proxy/common.js @@ -1,6 +1,5 @@ var common = exports, url = require('url'), - extend = require('util')._extend, required = require('requires-port'); var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i, @@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) { ); outgoing.method = options.method || req.method; - outgoing.headers = extend({}, req.headers); + outgoing.headers = Object.assign({}, req.headers); if (options.headers){ - extend(outgoing.headers, options.headers); + Object.assign(outgoing.headers, options.headers); } if (options.auth) { diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644 --- a/lib/http-proxy/index.js +++ b/lib/http-proxy/index.js @@ -1,5 +1,4 @@ var httpProxy = module.exports, - extend = require('util')._extend, parse_url = require('url').parse, EE3 = require('eventemitter3'), http = require('http'), @@ -47,9 +46,9 @@ function createRightProxy(type) { args[cntr] !== res ) { //Copy global options - requestOptions = extend({}, options); + requestOptions = Object.assign({}, options); //Overwrite with request options - extend(requestOptions, args[cntr]); + Object.assign(requestOptions, args[cntr]); cntr--; } ================================================ FILE: backend/src/constants.js ================================================ export const SCHEMA_VERSION_KEY = 'schemaVersion'; export const SETTINGS_KEY = 'settings'; export const SUBS_KEY = 'subs'; export const COLLECTIONS_KEY = 'collections'; export const FILES_KEY = 'files'; export const MODULES_KEY = 'modules'; export const ARTIFACTS_KEY = 'artifacts'; export const RULES_KEY = 'rules'; export const TOKENS_KEY = 'tokens'; export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup'; export const GIST_BACKUP_FILE_NAME = 'Sub-Store'; export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository'; export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource'; export const HEADERS_RESOURCE_CACHE_KEY = '#sub-store-cached-headers-resource'; export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; export const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour export const DEFAULT_HEADERS_CACHE_TTL = 60 * 1000; // 1 min export const DEFAULT_SCRIPT_CACHE_TTL = 48 * 3600 * 1000; // 48 hours ================================================ FILE: backend/src/core/app.js ================================================ import 'fastestsmallesttextencoderdecoder'; import { OpenAPI } from '@/vendor/open-api'; const $ = new OpenAPI('sub-store'); export default $; ================================================ FILE: backend/src/core/proxy-utils/index.js ================================================ import { Base64 } from 'js-base64'; import { Buffer } from 'buffer'; import rs from '@/utils/rs'; import YAML from '@/utils/yaml'; import download, { downloadFile } from '@/utils/download'; import { isIPv4, isIPv6, isValidPortNumber, isValidUUID, isNotBlank, ipAddress, getRandomPort, numberToString, } from '@/utils'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PREPROCESSORS from './preprocessors'; import PROXY_PRODUCERS from './producers'; import PROXY_PARSERS from './parsers'; import $ from '@/core/app'; import { FILES_KEY, MODULES_KEY } from '@/constants'; import { findByName } from '@/utils/database'; import { produceArtifact } from '@/restful/sync'; import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo'; import Gist from '@/utils/gist'; import { isPresent } from './producers/utils'; import { doh } from '@/utils/dns'; import JSON5 from 'json5'; function preprocess(raw) { for (const processor of PROXY_PREPROCESSORS) { try { if (processor.test(raw)) { $.info(`Pre-processor [${processor.name}] activated`); return processor.parse(raw); } } catch (e) { $.error(`Parser [${processor.name}] failed\n Reason: ${e}`); } } return raw; } function parse(raw) { raw = preprocess(raw); // parse const lines = raw.split('\n'); const proxies = []; let lastParser; for (let line of lines) { line = line.trim(); if (line.length === 0) continue; // skip empty line let success = false; // try to parse with last used parser if (lastParser) { const [proxy, error] = tryParse(lastParser, line); if (!error) { proxies.push(lastParse(proxy)); success = true; } } if (!success) { // search for a new parser for (const parser of PROXY_PARSERS) { const [proxy, error] = tryParse(parser, line); if (!error) { proxies.push(lastParse(proxy)); lastParser = parser; success = true; $.info(`${parser.name} is activated`); break; } } } if (!success) { $.error(`Failed to parse line: ${line}`); } } return proxies.filter((proxy) => { if (['vless', 'vmess'].includes(proxy.type)) { const isProxyUUIDValid = isValidUUID(proxy.uuid); if (!isProxyUUIDValid) { $.info(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`); } // return isProxyUUIDValid; } return true; }); } async function processFn( proxies, operators = [], targetPlatform, source, $options, ) { let context = {}; for (const item of operators) { if (item.disabled) { $.log( `Skipping disabled operator: "${ item.type }" with arguments:\n >>> ${ JSON.stringify(item.args, null, 2) || 'None' }`, ); continue; } // process script let script; let $arguments = {}; if (item.type.indexOf('Script') !== -1) { const { mode, content } = item.args; if (mode === 'link') { let url = content || ''; // extract link arguments const rawArgs = url.split('#'); if (rawArgs.length > 1) { try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` $arguments = JSON.parse(decodeURIComponent(rawArgs[1])); } catch (e) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; $arguments[key] = value == null || value === '' ? true : decodeURIComponent(value); } } } console.log(rawArgs); url = `${url.split('#')[0]}${ rawArgs[2] ? `#${rawArgs[2]}` : $arguments?.noCache != null || $arguments?.insecure != null ? `#${rawArgs[1]}` : '' }`; const downloadUrlMatch = url .split('#')[0] .match(/^\/api\/(file|module)\/(.+)/); if (downloadUrlMatch) { let type = ''; try { type = downloadUrlMatch?.[1]; let name = downloadUrlMatch?.[2]; if (name == null) { throw new Error(`本地 ${type} URL 无效: ${url}`); } name = decodeURIComponent(name); const key = type === 'module' ? MODULES_KEY : FILES_KEY; const item = findByName($.read(key), name); if (!item) { throw new Error(`找不到 ${type}: ${name}`); } if (type === 'module') { script = item.content; } else { script = await produceArtifact({ type: 'file', name, }); } } catch (err) { $.error( `Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`, ); throw new Error(`无法加载 ${type}: ${url}`); } } else if (url?.startsWith('/')) { try { const fs = eval(`require("fs")`); script = fs.readFileSync(url.split('#')[0], 'utf8'); // $.info(`Script loaded: >>>\n ${script}`); } catch (err) { $.error( `Error when reading local script: ${item.args.content}.\n Reason: ${err}`, ); throw new Error(`无法从该路径读取脚本文件: ${url}`); } } else { // if this is a remote script, download it try { script = await download(url); // $.info(`Script loaded: >>>\n ${script}`); } catch (err) { $.error( `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`, ); throw new Error(`无法下载脚本: ${url}`); } } } else { script = content; $arguments = item.args.arguments || {}; } } if (!PROXY_PROCESSORS[item.type]) { $.error(`Unknown operator: "${item.type}"`); continue; } $.log( `Applying "${item.type}" with arguments:\n >>> ${ JSON.stringify(item.args, null, 2) || 'None' }`, ); let processor; if (item.type.indexOf('Script') !== -1) { processor = PROXY_PROCESSORS[item.type]( script, targetPlatform, $arguments, source, $options, context, ); } else { processor = PROXY_PROCESSORS[item.type](item.args || {}); } proxies = await ApplyProcessor(processor, proxies); } return proxies; } function produce(proxies, targetPlatform, type, opts = {}) { const producer = PROXY_PRODUCERS[targetPlatform]; if (!producer) { throw new Error(`Target platform: ${targetPlatform} is not supported!`); } const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test( targetPlatform, ); // filter unsupported proxies proxies = proxies.filter((proxy) => { // 检查代理是否支持目标平台 if (proxy.supported && proxy.supported[targetPlatform] === false) { return false; } // 对于 vless 和 vmess 代理,需要额外验证 UUID if (['vless', 'vmess'].includes(proxy.type)) { const isProxyUUIDValid = isValidUUID(proxy.uuid); if (!isProxyUUIDValid) $.info(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`); // return isProxyUUIDValid; } return true; }); proxies = proxies.map((proxy) => { proxy._resolved = proxy.resolved; if (!isNotBlank(proxy.name)) { proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`; } if (proxy['disable-sni']) { if (sni_off_supported) { proxy.sni = 'off'; } else if (!['tuic'].includes(proxy.type)) { $.error( `Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`, ); proxy.sni = ''; proxy['skip-cert-verify'] = true; delete proxy['tls-fingerprint']; } } // 处理 端口跳跃 if (proxy.ports) { proxy.ports = String(proxy.ports); if (!['ClashMeta'].includes(targetPlatform)) { proxy.ports = proxy.ports.replace(/\//g, ','); } if (!proxy.port) { proxy.port = getRandomPort(proxy.ports); } } return proxy; }); $.log(`Producing proxies for target: ${targetPlatform}`); if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { let list = proxies .map((proxy) => { try { return producer.produce(proxy, type, opts); } catch (err) { $.error( `Cannot produce proxy: ${JSON.stringify( proxy, null, 2, )}\nReason: ${err}`, ); return ''; } }) .filter((line) => line.length > 0); list = type === 'internal' ? list : list.join('\n'); if ( targetPlatform.startsWith('Surge') && proxies.length > 0 && proxies.every((p) => p.type === 'wireguard') ) { list = `#!name=${proxies[0]?._subName} #!desc=${proxies[0]?._desc ?? ''} #!category=${proxies[0]?._category ?? ''} ${list}`; } return list; } else if (producer.type === 'ALL') { return producer.produce(proxies, type, opts); } } export const ProxyUtils = { parse, process: processFn, produce, ipAddress, getRandomPort, isIPv4, isIPv6, isIP, yaml: YAML, getFlag, removeFlag, getISO, MMDB, Gist, download, downloadFile, isValidUUID, doh, Buffer, Base64, JSON5, }; function tryParse(parser, line) { if (!safeMatch(parser, line)) return [null, new Error('Parser mismatch')]; try { const proxy = parser.parse(line); return [proxy, null]; } catch (err) { return [null, err]; } } function safeMatch(parser, line) { try { return parser.test(line); } catch (err) { return false; } } function formatTransportPath(path) { if (typeof path === 'string' || typeof path === 'number') { path = String(path).trim(); if (path === '') { return '/'; } else if (!path.startsWith('/')) { return '/' + path; } } return path; } function lastParse(proxy) { if (typeof proxy.cipher === 'string') { proxy.cipher = proxy.cipher.toLowerCase(); } if (typeof proxy.password === 'number') { proxy.password = numberToString(proxy.password); } if ( ['ss'].includes(proxy.type) && proxy.cipher === 'none' && !proxy.password ) { // https://github.com/MetaCubeX/mihomo/issues/1677 proxy.password = ''; } if (proxy.interface) { proxy['interface-name'] = proxy.interface; delete proxy.interface; } if (isValidPortNumber(proxy.port)) { proxy.port = parseInt(proxy.port, 10); } if (proxy.server) { proxy.server = `${proxy.server}` .trim() .replace(/^\[/, '') .replace(/\]$/, ''); } if (proxy.network === 'ws') { if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) { proxy['ws-opts'] = {}; if (proxy['ws-path']) { proxy['ws-opts'].path = proxy['ws-path']; } if (proxy['ws-headers']) { proxy['ws-opts'].headers = proxy['ws-headers']; } } delete proxy['ws-path']; delete proxy['ws-headers']; } const transportPath = proxy[`${proxy.network}-opts`]?.path; if (Array.isArray(transportPath)) { proxy[`${proxy.network}-opts`].path = transportPath.map((item) => formatTransportPath(item), ); } else if (transportPath != null) { proxy[`${proxy.network}-opts`].path = formatTransportPath(transportPath); } // network 逻辑有点乱了 可能还牵扯到别的逻辑 以后再优化... // 以 mihomo 为准的话, 其实应该是 // network¶ // 传输层,支持 ws/grpc,不配置或配置其他值则为 tcp if (proxy.type === 'trojan') { proxy.network = proxy.network || 'tcp'; } // network¶ // 传输层,支持 ws/http/h2/grpc,不配置或配置其他值则为 tcp if (['vmess'].includes(proxy.type)) { proxy.network = proxy.network || 'tcp'; proxy.cipher = proxy.cipher || 'none'; proxy.alterId = proxy.alterId || 0; } // network¶ // 传输层,支持 ws/http/h2/grpc,不配置或配置其他值则为 tcp if (['vless'].includes(proxy.type)) { proxy.network = proxy.network || 'tcp'; } if ( [ 'trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity', 'anytls', 'trusttunnel', 'naive', ].includes(proxy.type) ) { proxy.tls = true; } if (proxy.network) { let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host; if (proxy.network === 'h2') { if (!transporthost && transportHost) { proxy[`${proxy.network}-opts`].headers.host = transportHost; delete proxy[`${proxy.network}-opts`].headers.Host; } } else if (transporthost && !transportHost) { proxy[`${proxy.network}-opts`].headers.Host = transporthost; delete proxy[`${proxy.network}-opts`].headers.host; } } if (proxy.network === 'h2') { const host = proxy['h2-opts']?.headers?.host; const path = proxy['h2-opts']?.path; if (host && !Array.isArray(host)) { proxy['h2-opts'].headers.host = [host]; } if (Array.isArray(path)) { proxy['h2-opts'].path = path[0]; } } // 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host) if ( !proxy.tls && ['ws', 'http'].includes(proxy.network) && !proxy[`${proxy.network}-opts`]?.headers?.Host && !isIP(proxy.server) ) { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].headers = proxy[`${proxy.network}-opts`].headers || {}; proxy[`${proxy.network}-opts`].headers.Host = ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http' ? [proxy.server] : proxy.server; } // 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组 if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') { let transportPath = proxy[`${proxy.network}-opts`]?.path; let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; if (transportHost && !Array.isArray(transportHost)) { proxy[`${proxy.network}-opts`].headers.Host = [transportHost]; } if (transportPath && !Array.isArray(transportPath)) { proxy[`${proxy.network}-opts`].path = [transportPath]; } } if (proxy.tls && !proxy.sni) { if (!isIP(proxy.server)) { proxy.sni = proxy.server; } if (!proxy.sni && proxy.network) { let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; transportHost = Array.isArray(transportHost) ? transportHost[0] : transportHost; if (transportHost) { proxy.sni = transportHost; } } } // if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) { if (proxy.ports) { proxy.ports = String(proxy.ports).replace(/\//g, ','); } else { delete proxy.ports; } // } if ( ['hysteria2'].includes(proxy.type) && proxy.obfs && !['salamander'].includes(proxy.obfs) && !proxy['obfs-password'] ) { proxy['obfs-password'] = proxy.obfs; proxy.obfs = 'salamander'; } if ( ['hysteria2'].includes(proxy.type) && !proxy['obfs-password'] && proxy['obfs_password'] ) { proxy['obfs-password'] = proxy['obfs_password']; delete proxy['obfs_password']; } if (['vless'].includes(proxy.type)) { // 删除 reality-opts: {} if ( proxy['reality-opts'] && Object.keys(proxy['reality-opts']).length === 0 ) { delete proxy['reality-opts']; } // 删除 grpc-opts: {} if ( proxy['grpc-opts'] && Object.keys(proxy['grpc-opts']).length === 0 ) { delete proxy['grpc-opts']; } // 非 reality, 空 flow 没有意义 if ( (!proxy['reality-opts'] && !proxy.flow) || ['null', null].includes(proxy.flow) ) { delete proxy.flow; } if (['http'].includes(proxy.network)) { let transportPath = proxy[`${proxy.network}-opts`]?.path; if (!transportPath) { if (!proxy[`${proxy.network}-opts`]) { proxy[`${proxy.network}-opts`] = {}; } proxy[`${proxy.network}-opts`].path = ['/']; } } } if (typeof proxy.name !== 'string') { if (/^\d+$/.test(proxy.name)) { proxy.name = `${proxy.name}`; } else { try { if (proxy.name?.data) { proxy.name = Buffer.from(proxy.name.data).toString('utf8'); } else { proxy.name = Buffer.from(proxy.name).toString('utf8'); } } catch (e) { $.error(`proxy.name decode failed\nReason: ${e}`); proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`; } } } if (['ws', 'http', 'h2'].includes(proxy.network)) { if ( ['ws', 'h2'].includes(proxy.network) && !proxy[`${proxy.network}-opts`]?.path ) { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].path = '/'; } else if ( proxy.network === 'http' && (!Array.isArray(proxy[`${proxy.network}-opts`]?.path) || proxy[`${proxy.network}-opts`]?.path.every((i) => !i)) ) { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].path = ['/']; } } if (['', 'off'].includes(proxy.sni)) { proxy['disable-sni'] = true; } let caStr = proxy['ca_str']; if (proxy['ca-str']) { caStr = proxy['ca-str']; } else if (caStr) { delete proxy['ca_str']; proxy['ca-str'] = caStr; } try { if ($.env.isNode && !caStr && proxy['_ca']) { caStr = $.node.fs.readFileSync(proxy['_ca'], { encoding: 'utf8', }); } } catch (e) { $.error(`Read ca file failed\nReason: ${e}`); } if (!proxy['tls-fingerprint'] && caStr) { proxy['tls-fingerprint'] = rs.generateFingerprint(caStr); } if ( ['ss'].includes(proxy.type) && isPresent(proxy, 'shadow-tls-password') ) { proxy.plugin = 'shadow-tls'; proxy['plugin-opts'] = { host: proxy['shadow-tls-sni'], password: proxy['shadow-tls-password'], version: proxy['shadow-tls-version'], }; delete proxy['shadow-tls-sni']; delete proxy['shadow-tls-password']; delete proxy['shadow-tls-version']; } if (['tuic'].includes(proxy.type)) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn || 'h3']; proxy['congestion-controller'] = proxy['congestion-controller'] || 'cubic'; proxy['udp-relay-mode'] = proxy['udp-relay-mode'] || 'native'; } if (['wireguard'].includes(proxy.type)) { if (Array.isArray(proxy.peers) && proxy.peers.length > 0) { const validPeer = proxy.peers.find((peer) => peer.ip && peer.ipv6) || proxy.peers.find((peer) => peer.ip || peer.ipv6); if (validPeer) { if (!proxy.ip) { proxy.ip = proxy.peers[0]?.ip; } if (!proxy.ipv6) { proxy.ipv6 = proxy.peers[0]?.ipv6; } } } if (proxy.ip?.includes('/')) { const [ip] = proxy.ip.split('/'); if (isIPv4(ip)) { proxy.ip = ip; } } if (proxy.ipv6?.includes('/')) { const [ip] = proxy.ipv6.split('/'); if (isIPv6(ip)) { proxy.ipv6 = ip; } } } return proxy; } function isIP(ip) { return isIPv4(ip) || isIPv6(ip); } ================================================ FILE: backend/src/core/proxy-utils/parsers/index.js ================================================ import { isIPv4, isIPv6, getIfNotBlank, isPresent, isNotBlank, getIfPresent, getRandomPort, } from '@/utils'; import getSurgeParser from './peggy/surge'; import getLoonParser from './peggy/loon'; import getQXParser from './peggy/qx'; import getTrojanURIParser from './peggy/trojan-uri'; import $ from '@/core/app'; import JSON5 from 'json5'; import YAML from '@/utils/yaml'; import _ from 'lodash'; import { Base64 } from 'js-base64'; function surge_port_hopping(raw) { const [parts, port_hopping] = raw.match( /,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/, ) || []; return { port_hopping: port_hopping ? port_hopping.replace(/;/g, ',') : undefined, line: parts ? raw.replace(parts, '') : raw, }; } function URI_PROXY() { // socks5+tls // socks5 // http, https(可以这么写) const name = 'URI PROXY Parser'; const test = (line) => { return /^(socks5\+tls|socks5|http|https):\/\//.test(line); }; const parse = (line) => { // parse url // eslint-disable-next-line no-unused-vars let [__, type, tls, username, password, server, port, query, name] = line.match( /^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?\/?(\?.*?)?(?:#(.*?))?$/, ); if (port) { port = parseInt(port, 10); } else { if (tls) { port = 443; } else if (type === 'http') { port = 80; } else { $.error(`port is not present in line: ${line}`); throw new Error(`port is not present in line: ${line}`); } $.info(`port is not present in line: ${line}, set to ${port}`); } const proxy = { name: name != null ? decodeURIComponent(name) : `${type} ${server}:${port}`, type, tls: tls ? true : false, server, port, username: username != null ? decodeURIComponent(username) : undefined, password: password != null ? decodeURIComponent(password) : undefined, }; return proxy; }; return { name, test, parse }; } function URI_SOCKS() { const name = 'URI SOCKS Parser'; const test = (line) => { return /^socks:\/\//.test(line); }; const parse = (line) => { // parse url // eslint-disable-next-line no-unused-vars let [__, type, auth, server, port, query, name] = line.match( /^(socks)?:\/\/(?:(.*)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/, ); if (port) { port = parseInt(port, 10); } else { $.error(`port is not present in line: ${line}`); throw new Error(`port is not present in line: ${line}`); } let username, password; if (auth) { const parsed = Base64.decode(decodeURIComponent(auth)).split(':'); username = parsed[0]; password = parsed[1]; } const proxy = { name: name != null ? decodeURIComponent(name) : `${type} ${server}:${port}`, type: 'socks5', server, port, username, password, }; return proxy; }; return { name, test, parse }; } // Parse SS URI format (only supports new SIP002, legacy format is depreciated). // reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme function URI_SS() { const name = 'URI SS Parser'; const test = (line) => { return /^ss:\/\//.test(line); }; const parse = (line) => { // parse url let content = line.split('ss://')[1]; let name = line.split('#')[1]; const proxy = { type: 'ss', }; content = content.split('#')[0]; // strip proxy name // handle IPV4 and IPV6 let serverAndPortArray = content.match(/@([^/?]*)(\/|\?|$)/); let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大 let userInfoStr; if (rawUserInfoStr?.startsWith('2022-blake3-')) { userInfoStr = rawUserInfoStr; } else { userInfoStr = Base64.decode(rawUserInfoStr); } let query = ''; if (!serverAndPortArray) { if (content.includes('?')) { const parsed = content.match(/^(.*)(\?.*)$/); content = parsed[1]; query = parsed[2]; } content = Base64.decode(content); if (query) { if (/(&|\?)v2ray-plugin=/.test(query)) { const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/); let v2rayPlugin = parsed[2]; if (v2rayPlugin) { proxy.plugin = 'v2ray-plugin'; proxy['plugin-opts'] = JSON.parse( Base64.decode(v2rayPlugin), ); } } content = `${content}${query}`; } userInfoStr = content.match(/(^.*)@/)?.[1]; serverAndPortArray = content.match(/@([^/@]*)(\/|$)/); } else if (content.includes('?')) { const parsed = content.match(/(\?.*)$/); query = parsed[1]; } const params = {}; for (const addon of query.replace(/^\?/, '').split('&')) { if (addon) { const [key, valueRaw] = addon.split('='); let value = valueRaw; value = decodeURIComponent(valueRaw); params[key] = value; } } proxy.tls = params.security && params.security !== 'none'; proxy['skip-cert-verify'] = !!params['allowInsecure']; proxy.sni = params['sni'] || params['peer']; proxy['client-fingerprint'] = params.fp; proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined; if (params['ws']) { proxy.network = 'ws'; _.set(proxy, 'ws-opts.path', params['wspath']); } if (params['type']) { let httpupgrade; proxy.network = params['type']; if (proxy.network === 'httpupgrade') { proxy.network = 'ws'; httpupgrade = true; } if (['grpc'].includes(proxy.network)) { proxy[proxy.network + '-opts'] = { 'grpc-service-name': params['serviceName'], '_grpc-type': params['mode'], '_grpc-authority': params['authority'], }; } else { if (params['path']) { _.set( proxy, proxy.network + '-opts.path', decodeURIComponent(params['path']), ); } if (params['host']) { _.set( proxy, proxy.network + '-opts.headers.Host', decodeURIComponent(params['host']), ); } if (httpupgrade) { _.set( proxy, proxy.network + '-opts.v2ray-http-upgrade', true, ); _.set( proxy, proxy.network + '-opts.v2ray-http-upgrade-fast-open', true, ); } } if (['reality'].includes(params.security)) { const opts = {}; if (params.pbk) { opts['public-key'] = params.pbk; } if (params.sid) { opts['short-id'] = params.sid; } if (params.spx) { opts['_spider-x'] = params.spx; } if (params.mode) { proxy._mode = params.mode; } if (params.extra) { proxy._extra = params.extra; } if (Object.keys(opts).length > 0) { _.set(proxy, params.security + '-opts', opts); } } } proxy.udp = !!params['udp']; const serverAndPort = serverAndPortArray[1]; const portIdx = serverAndPort.lastIndexOf(':'); proxy.server = serverAndPort.substring(0, portIdx); proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match( /\d+/, )?.[0]; let userInfo = userInfoStr.match(/(^.*?):(.*$)/); proxy.cipher = userInfo?.[1]; proxy.password = userInfo?.[2]; // if (!proxy.cipher || !proxy.password) { // userInfo = rawUserInfoStr.match(/(^.*?):(.*$)/); // proxy.cipher = userInfo?.[1]; // proxy.password = userInfo?.[2]; // } // handle obfs const pluginMatch = content.match(/[?&]plugin=([^&]+)/); const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/); if (pluginMatch) { const pluginInfo = ( 'plugin=' + decodeURIComponent(pluginMatch[1]) ).split(';'); const params = {}; for (const item of pluginInfo) { const [key, val] = item.split('='); if (key) params[key] = val || true; // some options like "tls" will not have value } switch (params.plugin) { case 'obfs-local': case 'simple-obfs': proxy.plugin = 'obfs'; proxy['plugin-opts'] = { mode: params.obfs, host: getIfNotBlank(params['obfs-host']), }; break; case 'v2ray-plugin': proxy.plugin = 'v2ray-plugin'; proxy['plugin-opts'] = { mode: 'websocket', host: getIfNotBlank(params['obfs-host']) || getIfNotBlank(params['host']), path: getIfNotBlank(params.path), tls: getIfPresent(params.tls), }; break; case 'shadow-tls': { proxy.plugin = 'shadow-tls'; const version = getIfNotBlank(params['version']); proxy['plugin-opts'] = { host: getIfNotBlank(params['host']), password: getIfNotBlank(params['password']), version: version ? parseInt(version, 10) : undefined, }; break; } default: throw new Error( `Unsupported plugin option: ${params.plugin}`, ); } } // Shadowrocket if (shadowTlsMatch) { const params = JSON.parse(Base64.decode(shadowTlsMatch[1])); const version = getIfNotBlank(params['version']); const address = getIfNotBlank(params['address']); const port = getIfNotBlank(params['port']); proxy.plugin = 'shadow-tls'; proxy['plugin-opts'] = { host: getIfNotBlank(params['host']), password: getIfNotBlank(params['password']), version: version ? parseInt(version, 10) : undefined, }; if (address) { proxy.server = address; } if (port) { proxy.port = parseInt(port, 10); } } if (/(&|\?)uot=(1|true)/i.test(query)) { proxy['udp-over-tcp'] = true; } if (/(&|\?)tfo=(1|true)/i.test(query)) { proxy.tfo = true; } if (name != null) { name = decodeURIComponent(name); } proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`; return proxy; }; return { name, test, parse }; } // Parse URI SSR format, such as ssr://xxx function URI_SSR() { const name = 'URI SSR Parser'; const test = (line) => { return /^ssr:\/\//.test(line); }; const parse = (line) => { line = Base64.decode(line.split('ssr://')[1]); // handle IPV6 & IPV4 format let splitIdx = line.indexOf(':origin'); if (splitIdx === -1) { splitIdx = line.indexOf(':auth_'); } const serverAndPort = line.substring(0, splitIdx); const server = serverAndPort.substring( 0, serverAndPort.lastIndexOf(':'), ); const port = serverAndPort.substring( serverAndPort.lastIndexOf(':') + 1, ); let params = line .substring(splitIdx + 1) .split('/?')[0] .split(':'); let proxy = { type: 'ssr', server, port, protocol: params[0], cipher: params[1], obfs: params[2], password: Base64.decode(params[3]), }; // get other params const other_params = {}; line = line.split('/?')[1].split('&'); if (line.length > 1) { for (const item of line) { let [key, val] = item.split('='); val = val.trim(); if (val.length > 0 && val !== '(null)') { other_params[key] = val; } } } proxy = { ...proxy, name: other_params.remarks ? Base64.decode(other_params.remarks) : proxy.server, 'protocol-param': getIfNotBlank( Base64.decode(other_params.protoparam || '').replace(/\s/g, ''), ), 'obfs-param': getIfNotBlank( Base64.decode(other_params.obfsparam || '').replace(/\s/g, ''), ), }; return proxy; }; return { name, test, parse }; } // V2rayN URI VMess format // reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) // Quantumult VMess format function URI_VMess() { const name = 'URI VMess Parser'; const test = (line) => { return /^vmess:\/\//.test(line); }; const parse = (line) => { line = line.split('vmess://')[1]; let content = Base64.decode(line.replace(/\?.*?$/, '')); if (/=\s*vmess/.test(content)) { // Quantumult VMess URI format const partitions = content.split(',').map((p) => p.trim()); // get keyword params const params = {}; for (const part of partitions) { if (part.indexOf('=') !== -1) { const [key, val] = part.split('='); params[key.trim()] = val.trim(); } } const proxy = { name: partitions[0].split('=')[0].trim(), type: 'vmess', server: partitions[1], port: partitions[2], cipher: getIfNotBlank(partitions[3], 'auto'), uuid: partitions[4].match(/^"(.*)"$/)[1], tls: params.obfs === 'wss', udp: getIfPresent(params['udp-relay']), tfo: getIfPresent(params['fast-open']), 'skip-cert-verify': isPresent(params['tls-verification']) ? !params['tls-verification'] : undefined, }; // handle ws headers if (isPresent(params.obfs)) { if (params.obfs === 'ws' || params.obfs === 'wss') { proxy.network = 'ws'; proxy['ws-opts'].path = ( getIfNotBlank(params['obfs-path']) || '"/"' ).match(/^"(.*)"$/)[1]; let obfs_host = params['obfs-header']; if (obfs_host && obfs_host.indexOf('Host') !== -1) { obfs_host = obfs_host.match( /Host:\s*([a-zA-Z0-9-.]*)/, )[1]; } if (isNotBlank(obfs_host)) { proxy['ws-opts'].headers = { Host: obfs_host, }; } } else { throw new Error(`Unsupported obfs: ${params.obfs}`); } } return proxy; } else { let params = {}; try { // V2rayN URI format params = JSON.parse(content); } catch (e) { // Shadowrocket URI format // eslint-disable-next-line no-unused-vars let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line); content = Base64.decode(base64Line); for (const addon of qs.split('&')) { const [key, valueRaw] = addon.split('='); let value = valueRaw; value = decodeURIComponent(valueRaw); if (value.indexOf(',') === -1) { params[key] = value; } else { params[key] = value.split(','); } } // eslint-disable-next-line no-unused-vars let [___, cipher, uuid, server, port] = /(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content); params.scy = cipher; params.id = uuid; params.port = port; params.add = server; } const server = params.add; const port = parseInt(getIfPresent(params.port), 10); const proxy = { name: params.ps ?? params.remarks ?? params.remark ?? `VMess ${server}:${port}`, type: 'vmess', server, port, // https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link // https://github.com/XTLS/Xray-core/issues/91 cipher: [ 'auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', ].includes(params.scy) ? params.scy : 'auto', uuid: params.id, alterId: parseInt( getIfPresent(params.aid ?? params.alterId, 0), 10, ), tls: ['tls', true, 1, '1'].includes(params.tls), 'skip-cert-verify': isPresent(params.verify_cert) ? !params.verify_cert : undefined, }; if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) { proxy['skip-cert-verify'] = /(TRUE)|1/i.test( params.allowInsecure, ); } // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) if (proxy.tls) { if (params.sni && params.sni !== '') { proxy.sni = params.sni; } else if (params.peer && params.peer !== '') { proxy.sni = params.peer; } } let httpupgrade = false; // handle obfs if (params.net === 'ws' || params.obfs === 'websocket') { proxy.network = 'ws'; } else if ( ['http'].includes(params.net) || ['http'].includes(params.obfs) || ['http'].includes(params.type) ) { proxy.network = 'http'; } else if (['grpc', 'kcp', 'quic'].includes(params.net)) { proxy.network = params.net; } else if ( params.net === 'httpupgrade' || proxy.network === 'httpupgrade' ) { proxy.network = 'ws'; httpupgrade = true; } else if (params.net === 'h2' || proxy.network === 'h2') { proxy.network = 'h2'; } // 暂不支持 tcp + host + path // else if (params.net === 'tcp' || proxy.network === 'tcp') { // proxy.network = 'tcp'; // } if (proxy.network) { let transportHost = params.host ?? params.obfsParam; try { const parsedObfs = JSON.parse(transportHost); const parsedHost = parsedObfs?.Host; if (parsedHost) { transportHost = parsedHost; } // eslint-disable-next-line no-empty } catch (e) {} let transportPath = params.path; // 补上默认 path if (['ws'].includes(proxy.network)) { transportPath = transportPath || '/'; } if (proxy.network === 'http') { if (transportHost) { // 1)http(tcp)->host中间逗号(,)隔开 transportHost = transportHost .split(',') .map((i) => i.trim()); transportHost = Array.isArray(transportHost) ? transportHost[0] : transportHost; } if (transportPath) { transportPath = Array.isArray(transportPath) ? transportPath[0] : transportPath; } else { transportPath = '/'; } } // 传输层应该有配置, 暂时不考虑兼容不给配置的节点 if ( transportPath || transportHost || ['kcp', 'quic'].includes(proxy.network) ) { if (['grpc'].includes(proxy.network)) { proxy[`${proxy.network}-opts`] = { 'grpc-service-name': getIfNotBlank(transportPath), '_grpc-type': getIfNotBlank(params.type), '_grpc-authority': getIfNotBlank(params.authority), }; } else if (['kcp', 'quic'].includes(proxy.network)) { proxy[`${proxy.network}-opts`] = { [`_${proxy.network}-type`]: getIfNotBlank( params.type, ), [`_${proxy.network}-host`]: getIfNotBlank( getIfNotBlank(transportHost), ), [`_${proxy.network}-path`]: getIfNotBlank(transportPath), }; } else { const opts = { path: getIfNotBlank(transportPath), headers: { Host: getIfNotBlank(transportHost) }, }; if (httpupgrade) { opts['v2ray-http-upgrade'] = true; opts['v2ray-http-upgrade-fast-open'] = true; } proxy[`${proxy.network}-opts`] = opts; } } else { delete proxy.network; } } proxy['client-fingerprint'] = params.fp; proxy.alpn = params.alpn ? params.alpn.split(',') : undefined; // 然而 wiki 和 app 实测中都没有字段表示这个 // proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure); return proxy; } }; return { name, test, parse }; } function URI_VLESS() { const name = 'URI VLESS Parser'; const test = (line) => { return /^vless:\/\//.test(line); }; const parse = (line) => { line = line.split('vless://')[1]; let isShadowrocket; let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); if (!parsed) { // eslint-disable-next-line no-unused-vars let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line); line = `${Base64.decode(base64)}${other}`; parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); isShadowrocket = true; } // eslint-disable-next-line no-unused-vars let [__, uuid, server, port, ___, addons = '', name] = parsed; if (isShadowrocket) { uuid = uuid.replace(/^.*?:/g, ''); } port = parseInt(`${port}`, 10); uuid = decodeURIComponent(uuid); if (name != null) { name = decodeURIComponent(name); } const proxy = { type: 'vless', name, server, port, uuid, }; const params = {}; for (const addon of addons.split('&')) { if (addon) { const [key, valueRaw] = addon.split('='); let value = valueRaw; value = decodeURIComponent(valueRaw); params[key] = value; } } proxy.name = name ?? params.remarks ?? params.remark ?? `VLESS ${server}:${port}`; proxy.tls = params.security && params.security !== 'none'; if (isShadowrocket && /TRUE|1/i.test(params.tls)) { proxy.tls = true; params.security = params.security ?? 'reality'; } proxy.sni = params.sni || params.peer; proxy.flow = params.flow; if (!proxy.flow && isShadowrocket && params.xtls) { // "none" is undefined const flow = [undefined, 'xtls-rprx-direct', 'xtls-rprx-vision'][ params.xtls ]; if (flow) { proxy.flow = flow; } } proxy['client-fingerprint'] = params.fp; proxy.alpn = params.alpn ? params.alpn.split(',') : undefined; proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure); proxy._echConfigList = getIfPresent(params.ech); proxy._pcs = getIfPresent(params.pcs); proxy._h2 = /(TRUE)|1/i.test(params.h2); if (['reality'].includes(params.security)) { const opts = {}; if (params.pbk) { opts['public-key'] = params.pbk; } if (params.sid) { opts['short-id'] = params.sid; } if (params.spx) { opts['_spider-x'] = params.spx; } if (Object.keys(opts).length > 0) { // proxy[`${params.security}-opts`] = opts; proxy[`${params.security}-opts`] = opts; } } let httpupgrade = false; proxy.network = params.type; if (proxy.network === 'tcp' && params.headerType === 'http') { proxy.network = 'http'; } else if (proxy.network === 'httpupgrade') { proxy.network = 'ws'; httpupgrade = true; } if (!proxy.network && isShadowrocket && params.obfs) { proxy.network = params.obfs; if (['none'].includes(proxy.network)) { proxy.network = 'tcp'; } } if (['websocket'].includes(proxy.network)) { proxy.network = 'ws'; } if (proxy.network && !['tcp', 'none'].includes(proxy.network)) { const opts = {}; const host = params.host ?? params.obfsParam; if (host) { if (params.obfsParam) { try { const parsed = JSON.parse(host); opts.headers = parsed; } catch (e) { opts.headers = { Host: host }; } } else { opts.headers = { Host: host }; } } if (params.serviceName) { opts[`${proxy.network}-service-name`] = params.serviceName; if (['grpc'].includes(proxy.network) && params.authority) { opts['_grpc-authority'] = params.authority; } } else if (isShadowrocket && params.path) { if (!['ws', 'http', 'h2'].includes(proxy.network)) { opts[`${proxy.network}-service-name`] = params.path; delete params.path; } } if (params.path) { opts.path = params.path; } // https://github.com/XTLS/Xray-core/issues/91 if (['grpc'].includes(proxy.network)) { opts['_grpc-type'] = params.mode || 'gun'; } if (httpupgrade) { opts['v2ray-http-upgrade'] = true; opts['v2ray-http-upgrade-fast-open'] = true; } if (Object.keys(opts).length > 0) { proxy[`${proxy.network}-opts`] = opts; } if (proxy.network === 'kcp') { // mKCP 种子。省略时不使用种子,但不可以为空字符串。建议 mKCP 用户使用 seed。 if (params.seed) { proxy.seed = params.seed; } // mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none,即不使用伪装头部,但不可以为空字符串。 proxy.headerType = params.headerType || 'none'; } if (params.mode) { proxy._mode = params.mode; } if (params.extra) { proxy._extra = params.extra; } } if (params.encryption) { proxy.encryption = params.encryption; } if (params.pqv) { proxy._pqv = params.pqv; } return proxy; }; return { name, test, parse }; } function URI_AnyTLS() { const name = 'URI AnyTLS Parser'; const test = (line) => { return /^anytls:\/\//.test(line); }; const parse = (line) => { const parsed = URI_VLESS().parse(line.replace('anytls', 'vless')); // 偷个懒 line = line.split(/anytls:\/\//)[1]; // eslint-disable-next-line no-unused-vars let [__, password, server, port, addons = '', name] = /^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line); password = decodeURIComponent(password); port = parseInt(`${port}`, 10); if (isNaN(port)) { port = 443; } password = decodeURIComponent(password); if (name != null) { name = decodeURIComponent(name); } name = name ?? `AnyTLS ${server}:${port}`; const proxy = { ...parsed, uuid: undefined, type: 'anytls', name, server, port, password, }; for (const addon of addons.split('&')) { if (addon) { let [key, value] = addon.split('='); key = key.replace(/_/g, '-'); value = decodeURIComponent(value); if (['alpn'].includes(key)) { proxy[key] = value ? value.split(',') : undefined; } else if (['insecure'].includes(key)) { proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value); } else if (['udp'].includes(key)) { proxy[key] = /(TRUE)|1/i.test(value); } else if (!Object.keys(proxy).includes(key)) { proxy[key] = value; } } } if (['tcp'].includes(proxy.network) && !proxy['reality-opts']) { delete proxy.network; delete proxy.security; } return proxy; }; return { name, test, parse }; } function URI_Hysteria2() { const name = 'URI Hysteria2 Parser'; const test = (line) => { return /^(hysteria2|hy2):\/\//.test(line); }; const parse = (line) => { line = line.split(/(hysteria2|hy2):\/\//)[2]; // 端口跳跃有两种写法: // 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。 // 端口部分支持 端口跳跃 的「多端口地址格式」。 // https://hysteria.network/zh/docs/advanced/Port-Hopping // 2. 参数 mport let ports; /* eslint-disable no-unused-vars */ let [ __, password, server, ___, port, ____, _____, ______, _______, ________, addons = '', name, ] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec( line, ); /* eslint-enable no-unused-vars */ if (/^\d+$/.test(port)) { port = parseInt(`${port}`, 10); if (isNaN(port)) { port = 443; } } else if (port) { ports = port; port = getRandomPort(ports); } else { port = 443; } password = decodeURIComponent(password); if (name != null) { name = decodeURIComponent(name); } name = name ?? `Hysteria2 ${server}:${port}`; const proxy = { type: 'hysteria2', name, server, port, ports, password, }; const params = {}; for (const addon of addons.split('&')) { if (addon) { const [key, valueRaw] = addon.split('='); let value = valueRaw; value = decodeURIComponent(valueRaw); params[key] = value; } } proxy.sni = params.sni; if (!proxy.sni && params.peer) { proxy.sni = params.peer; } if (params.obfs && params.obfs !== 'none') { proxy.obfs = params.obfs; } if (params.mport) { proxy.ports = params.mport; } proxy['obfs-password'] = params['obfs-password']; proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure); proxy.tfo = /(TRUE)|1/i.test(params.fastopen); proxy['tls-fingerprint'] = params.pinSHA256; let hop_interval = params['hop-interval'] || params['hop_interval']; if (/^\d+$/.test(hop_interval)) { proxy['hop-interval'] = parseInt(`${hop_interval}`, 10); } let keepalive = params['keepalive']; if (/^\d+$/.test(keepalive)) { proxy['keepalive'] = parseInt(`${keepalive}`, 10); } return proxy; }; return { name, test, parse }; } function URI_Hysteria() { const name = 'URI Hysteria Parser'; const test = (line) => { return /^(hysteria|hy):\/\//.test(line); }; const parse = (line) => { line = line.split(/(hysteria|hy):\/\//)[2]; // eslint-disable-next-line no-unused-vars let [__, server, ___, port, ____, addons = '', name] = /^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); port = parseInt(`${port}`, 10); if (isNaN(port)) { port = 443; } if (name != null) { name = decodeURIComponent(name); } name = name ?? `Hysteria ${server}:${port}`; const proxy = { type: 'hysteria', name, server, port, }; const params = {}; for (const addon of addons.split('&')) { if (addon) { let [key, value] = addon.split('='); key = key.replace(/_/, '-'); value = decodeURIComponent(value); if (['alpn'].includes(key)) { proxy[key] = value ? value.split(',') : undefined; } else if (['insecure'].includes(key)) { proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value); } else if (['auth'].includes(key)) { proxy['auth-str'] = value; } else if (['mport'].includes(key)) { proxy['ports'] = value; } else if (['obfsParam'].includes(key)) { proxy['obfs'] = value; } else if (['upmbps'].includes(key)) { proxy['up'] = value; } else if (['downmbps'].includes(key)) { proxy['down'] = value; } else if (['obfs'].includes(key)) { // obfs: Obfuscation mode (optional, empty or "xplus") proxy['_obfs'] = value || ''; } else if (['fast-open', 'peer'].includes(key)) { params[key] = value; } else if (!Object.keys(proxy).includes(key)) { proxy[key] = value; } } } if (!proxy.sni && params.peer) { proxy.sni = params.peer; } if (!proxy['fast-open'] && params.fastopen) { proxy['fast-open'] = true; } if (!proxy.protocol) { // protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp") proxy.protocol = 'udp'; } return proxy; }; return { name, test, parse }; } function URI_TUIC() { const name = 'URI TUIC Parser'; const test = (line) => { return /^tuic:\/\//.test(line); }; const parse = (line) => { line = line.split(/tuic:\/\//)[1]; // eslint-disable-next-line no-unused-vars let [__, auth, server, port, addons = '', name] = /^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line); auth = decodeURIComponent(auth); let [uuid, ...passwordParts] = auth.split(':'); let password = passwordParts.join(':'); port = parseInt(`${port}`, 10); if (isNaN(port)) { port = 443; } password = decodeURIComponent(password); if (name != null) { name = decodeURIComponent(name); } name = name ?? `TUIC ${server}:${port}`; const proxy = { type: 'tuic', name, server, port, password, uuid, }; for (const addon of addons.split('&')) { if (addon) { let [key, value] = addon.split('='); key = key.replace(/_/g, '-'); value = decodeURIComponent(value); if (['alpn'].includes(key)) { proxy[key] = value ? value.split(',') : undefined; } else if (['allow-insecure', 'insecure'].includes(key)) { proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value); } else if (['fast-open'].includes(key)) { proxy.tfo = true; } else if (['disable-sni', 'reduce-rtt'].includes(key)) { proxy[key] = /(TRUE)|1/i.test(value); } else if (key === 'congestion-control') { proxy['congestion-controller'] = value; delete proxy[key]; } else if (!Object.keys(proxy).includes(key)) { proxy[key] = value; } } } return proxy; }; return { name, test, parse }; } function URI_WireGuard() { const name = 'URI WireGuard Parser'; const test = (line) => { return /^(wireguard|wg):\/\//.test(line); }; const parse = (line) => { line = line.split(/(wireguard|wg):\/\//)[2]; /* eslint-disable no-unused-vars */ let [ __, ___, privateKey, server, ____, port, _____, addons = '', name, ] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); /* eslint-enable no-unused-vars */ port = parseInt(`${port}`, 10); if (isNaN(port)) { port = 51820; } privateKey = decodeURIComponent(privateKey); if (name != null) { name = decodeURIComponent(name); } name = name ?? `WireGuard ${server}:${port}`; const proxy = { type: 'wireguard', name, server, port, 'private-key': privateKey, udp: true, }; for (const addon of addons.split('&')) { if (addon) { let [key, value] = addon.split('='); key = key.replace(/_/, '-'); value = decodeURIComponent(value); if (['reserved'].includes(key)) { const parsed = value .split(',') .map((i) => parseInt(i.trim(), 10)) .filter((i) => Number.isInteger(i)); if (parsed.length === 3) { proxy[key] = parsed; } } else if (['address', 'ip'].includes(key)) { value.split(',').map((i) => { const ip = i .trim() .replace(/\/\d+$/, '') .replace(/^\[/, '') .replace(/\]$/, ''); if (isIPv4(ip)) { proxy.ip = ip; } else if (isIPv6(ip)) { proxy.ipv6 = ip; } }); } else if (['mtu'].includes(key)) { const parsed = parseInt(value.trim(), 10); if (Number.isInteger(parsed)) { proxy[key] = parsed; } } else if (/publickey/i.test(key)) { proxy['public-key'] = value; } else if (/privatekey/i.test(key)) { proxy['private-key'] = value; } else if (['udp'].includes(key)) { proxy[key] = /(TRUE)|1/i.test(value); } else if (![...Object.keys(proxy), 'flag'].includes(key)) { proxy[key] = value; } } } return proxy; }; return { name, test, parse }; } // Trojan URI format function URI_Trojan() { const name = 'URI Trojan Parser'; const test = (line) => { return /^trojan:\/\//.test(line); }; const parse = (line) => { const matched = /^(trojan:\/\/.*?@.*?)(:(\d+))?\/?(\?.*?)?$/.exec(line); const port = matched?.[2]; if (!port) { line = line.replace(matched[1], `${matched[1]}:443`); } let [newLine, name] = line.split(/#(.+)/, 2); const parser = getTrojanURIParser(); const proxy = parser.parse(newLine); if (isNotBlank(name)) { try { proxy.name = decodeURIComponent(name); } catch (e) { console.log(e); } } return proxy; }; return { name, test, parse }; } function Clash_All() { const name = 'Clash Parser'; const test = (line) => { let proxy; try { proxy = JSON5.parse(line); } catch (e) { proxy = YAML.parse(line); } return !!proxy?.type; }; const parse = (line) => { let proxy; try { proxy = JSON5.parse(line); } catch (e) { proxy = YAML.parse(line); } if ( ![ 'trusttunnel', 'naive', 'anytls', 'mieru', 'masque', 'sudoku', 'juicity', 'ss', 'ssr', 'vmess', 'socks5', 'http', 'snell', 'trojan', 'tuic', 'vless', 'hysteria', 'hysteria2', 'wireguard', 'ssh', 'direct', ].includes(proxy.type) ) { throw new Error( `Clash does not support proxy with type: ${proxy.type}`, ); } // handle vmess sni if (['vmess', 'vless'].includes(proxy.type) && proxy.servername) { proxy.sni = proxy.servername; delete proxy.servername; } if (proxy['server-cert-fingerprint']) { proxy['tls-fingerprint'] = proxy['server-cert-fingerprint']; } if (proxy.fingerprint) { proxy['tls-fingerprint'] = proxy.fingerprint; } if (proxy['dialer-proxy']) { proxy['underlying-proxy'] = proxy['dialer-proxy']; } if (proxy['benchmark-url']) { proxy['test-url'] = proxy['benchmark-url']; } if (proxy['benchmark-timeout']) { proxy['test-timeout'] = proxy['benchmark-timeout']; } return proxy; }; return { name, test, parse }; } function QX_SS() { const name = 'QX SS Parser'; const test = (line) => { return ( /^shadowsocks\s*=/.test(line.split(',')[0].trim()) && line.indexOf('ssr-protocol') === -1 ); }; const parse = (line) => { const parser = getQXParser(); return parser.parse(line); }; return { name, test, parse }; } function QX_SSR() { const name = 'QX SSR Parser'; const test = (line) => { return ( /^shadowsocks\s*=/.test(line.split(',')[0].trim()) && line.indexOf('ssr-protocol') !== -1 ); }; const parse = (line) => getQXParser().parse(line); return { name, test, parse }; } function QX_VMess() { const name = 'QX VMess Parser'; const test = (line) => { return /^vmess\s*=/.test(line.split(',')[0].trim()); }; const parse = (line) => getQXParser().parse(line); return { name, test, parse }; } function QX_VLESS() { const name = 'QX VLESS Parser'; const test = (line) => { return /^vless\s*=/.test(line.split(',')[0].trim()); }; const parse = (line) => getQXParser().parse(line); return { name, test, parse }; } function QX_Trojan() { const name = 'QX Trojan Parser'; const test = (line) => { return /^trojan\s*=/.test(line.split(',')[0].trim()); }; const parse = (line) => getQXParser().parse(line); return { name, test, parse }; } function QX_Http() { const name = 'QX HTTP Parser'; const test = (line) => { return /^http\s*=/.test(line.split(',')[0].trim()); }; const parse = (line) => getQXParser().parse(line); return { name, test, parse }; } function QX_Socks5() { const name = 'QX Socks5 Parser'; const test = (line) => { return /^socks5\s*=/.test(line.split(',')[0].trim()); }; const parse = (line) => getQXParser().parse(line); return { name, test, parse }; } function Loon_SS() { const name = 'Loon SS Parser'; const test = (line) => { return ( line.split(',')[0].split('=')[1].trim().toLowerCase() === 'shadowsocks' ); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_SSR() { const name = 'Loon SSR Parser'; const test = (line) => { return ( line.split(',')[0].split('=')[1].trim().toLowerCase() === 'shadowsocksr' ); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_VMess() { const name = 'Loon VMess Parser'; const test = (line) => { // distinguish between surge vmess return ( /^.*=\s*vmess/i.test(line.split(',')[0]) && line.indexOf('username') === -1 ); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_Vless() { const name = 'Loon Vless Parser'; const test = (line) => { return /^.*=\s*vless/i.test(line.split(',')[0]); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_Trojan() { const name = 'Loon Trojan Parser'; const test = (line) => { return /^.*=\s*trojan/i.test(line.split(',')[0]); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_AnyTLS() { const name = 'Loon AnyTLS Parser'; const test = (line) => { return /^.*=\s*anytls/i.test(line.split(',')[0]); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_Hysteria2() { const name = 'Loon Hysteria2 Parser'; const test = (line) => { return /^.*=\s*Hysteria2/i.test(line.split(',')[0]); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_Http() { const name = 'Loon HTTP Parser'; const test = (line) => { return /^.*=\s*http/i.test(line.split(',')[0]); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_Socks5() { const name = 'Loon SOCKS5 Parser'; const test = (line) => { return /^.*=\s*socks5/i.test(line.split(',')[0]); }; const parse = (line) => getLoonParser().parse(line); return { name, test, parse }; } function Loon_WireGuard() { const name = 'Loon WireGuard Parser'; const test = (line) => { return /^.*=\s*wireguard/i.test(line.split(',')[0]); }; const parse = (line) => { const name = line.match( /(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i, )?.[1]; line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, ''); let peers = line.match( /,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i, )?.[1]; let serverPort = peers.match( /(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i, ); let server = serverPort?.[2]; let port = parseInt(serverPort?.[3], 10); let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2]; if (mtu) { mtu = parseInt(mtu, 10); } let keepalive = line.match( /(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i, )?.[2]; if (keepalive) { keepalive = parseInt(keepalive, 10); } let reserved = peers.match( /(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i, )?.[2]; if (reserved) { reserved = JSON.parse(reserved); } let dns; let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2]; let dnsv6 = line.match( /(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, )?.[2]; if (dnsv4 || dnsv6) { dns = []; if (dnsv4) { dns.push(dnsv4); } if (dnsv6) { dns.push(dnsv6); } } let allowedIps = peers .match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2] ?.split(',') .map((i) => i.trim()); let preSharedKey = peers.match( /(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, )?.[2]; let ip = line.match( /(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, )?.[2]; let ipv6 = line.match( /(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, )?.[2]; let publicKey = peers.match( /(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, )?.[2]; // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717 const proxy = { type: 'wireguard', name, server, port, ip, ipv6, 'private-key': line.match( /(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i, )?.[2], 'public-key': publicKey, mtu, keepalive, reserved, 'allowed-ips': allowedIps, 'preshared-key': preSharedKey, dns, udp: true, peers: [ { server, port, ip, ipv6, 'public-key': publicKey, 'pre-shared-key': preSharedKey, 'allowed-ips': allowedIps, reserved, }, ], }; proxy; if (Array.isArray(proxy.dns) && proxy.dns.length > 0) { proxy['remote-dns-resolve'] = true; } return proxy; }; return { name, test, parse }; } function Surge_Direct() { const name = 'Surge Direct Parser'; const test = (line) => { return /^.*=\s*direct/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_AnyTLS() { const name = 'Surge AnyTLS Parser'; const test = (line) => { return /^.*=\s*anytls/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_TrustTunnel() { const name = 'Surge TrustTunnel Parser'; const test = (line) => { return /^.*=\s*trust-tunnel/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_SSH() { const name = 'Surge SSH Parser'; const test = (line) => { return /^.*=\s*ssh/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_SS() { const name = 'Surge SS Parser'; const test = (line) => { return /^.*=\s*ss/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_VMess() { const name = 'Surge VMess Parser'; const test = (line) => { return ( /^.*=\s*vmess/.test(line.split(',')[0]) && line.indexOf('username') !== -1 ); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_Trojan() { const name = 'Surge Trojan Parser'; const test = (line) => { return /^.*=\s*trojan/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_Http() { const name = 'Surge HTTP Parser'; const test = (line) => { return /^.*=\s*https?/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_Socks5() { const name = 'Surge Socks5 Parser'; const test = (line) => { return /^.*=\s*socks5(-tls)?/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_External() { const name = 'Surge External Parser'; const test = (line) => { return /^.*=\s*external/.test(line.split(',')[0]); }; const parse = (line) => { let parsed = /^\s*(.*?)\s*?=\s*?external\s*?,\s*(.*?)\s*$/.exec(line); // eslint-disable-next-line no-unused-vars let [_, name, other] = parsed; line = other; // exec = "/usr/bin/ssh" 或 exec = /usr/bin/ssh let exec = /(,|^)\s*?exec\s*?=\s*"(.*?)"\s*?(,|$)/.exec(line)?.[2]; if (!exec) { exec = /(,|^)\s*?exec\s*?=\s*(.*?)\s*?(,|$)/.exec(line)?.[2]; } // local-port = "1080" 或 local-port = 1080 let localPort = /(,|^)\s*?local-port\s*?=\s*"(.*?)"\s*?(,|$)/.exec( line, )?.[2]; if (!localPort) { localPort = /(,|^)\s*?local-port\s*?=\s*(.*?)\s*?(,|$)/.exec( line, )?.[2]; } // args = "-m", args = "rc4-md5" // args = -m, args = rc4-md5 const argsRegex = /(,|^)\s*?args\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g; let argsMatch; const args = []; while ((argsMatch = argsRegex.exec(line)) !== null) { if (argsMatch[3] != null) { args.push(argsMatch[3]); } else if (argsMatch[4] != null) { args.push(argsMatch[4]); } } // addresses = "[ipv6]",,addresses = "ipv6", addresses = "ipv4" // addresses = [ipv6], addresses = ipv6, addresses = ipv4 const addressesRegex = /(,|^)\s*?addresses\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g; let addressesMatch; const addresses = []; while ((addressesMatch = addressesRegex.exec(line)) !== null) { let ip; if (addressesMatch[3] != null) { ip = addressesMatch[3]; } else if (addressesMatch[4] != null) { ip = addressesMatch[4]; } if (ip != null) { ip = `${ip}`.trim().replace(/^\[/, '').replace(/\]$/, ''); } if (isIP(ip)) { addresses.push(ip); } } const proxy = { type: 'external', name, exec, 'local-port': localPort, args, addresses, }; return proxy; }; return { name, test, parse }; } function Surge_Snell() { const name = 'Surge Snell Parser'; const test = (line) => { return /^.*=\s*snell/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_Tuic() { const name = 'Surge Tuic Parser'; const test = (line) => { return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]); }; const parse = (raw) => { const { port_hopping, line } = surge_port_hopping(raw); const proxy = getSurgeParser().parse(line); proxy['ports'] = port_hopping; return proxy; }; return { name, test, parse }; } function Surge_WireGuard() { const name = 'Surge WireGuard Parser'; const test = (line) => { return /^.*=\s*wireguard/.test(line.split(',')[0]); }; const parse = (line) => getSurgeParser().parse(line); return { name, test, parse }; } function Surge_Hysteria2() { const name = 'Surge Hysteria2 Parser'; const test = (line) => { return /^.*=\s*hysteria2/.test(line.split(',')[0]); }; const parse = (raw) => { const { port_hopping, line } = surge_port_hopping(raw); const proxy = getSurgeParser().parse(line); proxy['ports'] = port_hopping; return proxy; }; return { name, test, parse }; } function isIP(ip) { return isIPv4(ip) || isIPv6(ip); } export default [ URI_PROXY(), URI_SOCKS(), URI_SS(), URI_SSR(), URI_VMess(), URI_VLESS(), URI_TUIC(), URI_WireGuard(), URI_Hysteria(), URI_Hysteria2(), URI_Trojan(), URI_AnyTLS(), Clash_All(), Surge_Direct(), Surge_AnyTLS(), Surge_TrustTunnel(), Surge_SSH(), Surge_SS(), Surge_VMess(), Surge_Trojan(), Surge_Http(), Surge_Snell(), Surge_Tuic(), Surge_WireGuard(), Surge_Hysteria2(), Surge_Socks5(), Surge_External(), Loon_SS(), Loon_SSR(), Loon_VMess(), Loon_Vless(), Loon_Hysteria2(), Loon_Trojan(), Loon_AnyTLS(), Loon_Http(), Loon_Socks5(), Loon_WireGuard(), QX_SS(), QX_SSR(), QX_VMess(), QX_VLESS(), QX_Trojan(), QX_Http(), QX_Socks5(), ]; ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/loon.js ================================================ import * as peggy from 'peggy'; const grammars = String.raw` // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } }} // per-parser initializer { const proxy = {}; const obfs = {}; const transport = {}; const $ = {}; function handleTransport() { if (transport.type === "tcp") { /* do nothing */ } else if (transport.type === "ws") { proxy.network = "ws"; $set(proxy, "ws-opts.path", transport.path); $set(proxy, "ws-opts.headers.Host", transport.host); } else if (transport.type === "http") { proxy.network = "http"; $set(proxy, "http-opts.path", transport.path); $set(proxy, "http-opts.headers.Host", transport.host); } } } start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2/anytls) { return proxy; } shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{ proxy.type = "ssr"; // handle ssr obfs proxy.obfs = obfs.type; } shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/udp_over_tcp/others)* { proxy.type = "ss"; // handle ss obfs if (obfs.type == "http" || obfs.type === "tls") { proxy.plugin = "obfs"; $set(proxy, "plugin-opts.mode", obfs.type); $set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.path", obfs.path); } } vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; proxy.alterId = proxy.alterId || 0; handleTransport(); } vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* { proxy.type = "vless"; handleTransport(); } trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "trojan"; handleTransport(); } anytls = tag equals "anytls"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/idle_session_check_interval/idle_session_timeout/min_idle_session/max_stream_count/others)* { proxy.type = "anytls"; handleTransport(); } hysteria2 = tag equals "hysteria2"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* { proxy.type = "hysteria2"; } https = tag equals "https"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "http"; proxy.tls = true; } http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "http"; } socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "socks5"; } address = comma server:server comma port:port { proxy.server = server; proxy.port = port; } server = ip/domain ip = & { const start = peg$currPos; let j = start; while (j < input.length) { if (input[j] === ",") break; j++; } peg$currPos = j; $.ip = input.substring(start, j).trim(); return true; } { return $.ip; } domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } throw new Error("Invalid domain: " + domain); } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } throw new Error("Invalid port number: " + port); } method = comma cipher:cipher { proxy.cipher = cipher; } cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm"); username = & { let j = peg$currPos; let start, end; let first = true; while (j < input.length) { if (input[j] === ',') { if (first) { start = j + 1; first = false; } else { end = j; break; } } j++; } const match = input.substring(start, end); if (match.indexOf("=") === -1) { $.username = match; peg$currPos = end; return true; } } { proxy.username = $.username; } password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } obfs_typev = comma type:("http"/"tls") { obfs.type = type; } obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); } obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; } obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); } obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } uri = $[^,]+ transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; } transport_host = comma "host" equals match:[^,]+ { transport.host = match.join("").replace(/^"(.*)"$/, '$1'); } transport_path = comma "path" equals path:uri { transport.path = path; } ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; } ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); } shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } tls_name = comma sni:("tls-name") equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); } sni = comma "sni" equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); } flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); } public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); } short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; } block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; } idle_session_check_interval = comma "idle-session-check-interval" equals match:$[0-9]+ { proxy["idle-session-check-interval"] = parseInt(match.trim()); } idle_session_timeout = comma "idle-session-timeout" equals match:$[0-9]+ { proxy["idle-session-timeout"] = parseInt(match.trim()); } min_idle_session = comma "min-idle-session" equals match:$[0-9]+ { proxy["min-idle-session"] = parseInt(match.trim()); } max_stream_count = comma "max-stream-count" equals match:$[0-9]+ { proxy["max-stream-count"] = parseInt(match.trim()); } udp_over_tcp = comma "udp-over-tcp" equals flag:bool { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } tag = match:[^=,]* { proxy.name = match.join("").trim(); } comma = _ "," _ equals = _ "=" _ _ = [ \r\t]* bool = b:("true"/"false") { return b === "true" } others = comma [^=,]+ equals [^=,]+ `; let parser; export default function getParser() { if (!parser) { parser = peggy.generate(grammars); } return parser; } ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/loon.peg ================================================ // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } }} // per-parser initializer { const proxy = {}; const obfs = {}; const transport = {}; const $ = {}; function handleTransport() { if (transport.type === "tcp") { /* do nothing */ } else if (transport.type === "ws") { proxy.network = "ws"; $set(proxy, "ws-opts.path", transport.path); $set(proxy, "ws-opts.headers.Host", transport.host); } else if (transport.type === "http") { proxy.network = "http"; $set(proxy, "http-opts.path", transport.path); $set(proxy, "http-opts.headers.Host", transport.host); } } } start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2/anytls) { return proxy; } shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/udp_over_tcp/others)*{ proxy.type = "ssr"; // handle ssr obfs proxy.obfs = obfs.type; } shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)* { proxy.type = "ss"; // handle ss obfs if (obfs.type == "http" || obfs.type === "tls") { proxy.plugin = "obfs"; $set(proxy, "plugin-opts.mode", obfs.type); $set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.path", obfs.path); } } vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; proxy.alterId = proxy.alterId || 0; handleTransport(); } vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* { proxy.type = "vless"; handleTransport(); } trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "trojan"; handleTransport(); } anytls = tag equals "anytls"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/idle_session_check_interval/idle_session_timeout/min_idle_session/max_stream_count/others)* { proxy.type = "anytls"; handleTransport(); } hysteria2 = tag equals "hysteria2"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* { proxy.type = "hysteria2"; } https = tag equals "https"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "http"; proxy.tls = true; } http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "http"; } socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* { proxy.type = "socks5"; } address = comma server:server comma port:port { proxy.server = server; proxy.port = port; } server = ip/domain ip = & { const start = peg$currPos; let j = start; while (j < input.length) { if (input[j] === ",") break; j++; } peg$currPos = j; $.ip = input.substring(start, j).trim(); return true; } { return $.ip; } domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } throw new Error("Invalid domain: " + domain); } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } throw new Error("Invalid port number: " + port); } method = comma cipher:cipher { proxy.cipher = cipher; } cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm"); username = & { let j = peg$currPos; let start, end; let first = true; while (j < input.length) { if (input[j] === ',') { if (first) { start = j + 1; first = false; } else { end = j; break; } } j++; } const match = input.substring(start, end); if (match.indexOf("=") === -1) { $.username = match; peg$currPos = end; return true; } } { proxy.username = $.username; } password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } obfs_typev = comma type:("http"/"tls") { obfs.type = type; } obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); } obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; } obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); } obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } uri = $[^,]+ transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; } transport_host = comma "host" equals match:[^,]+ { transport.host = match.join("").replace(/^"(.*)"$/, '$1'); } transport_path = comma "path" equals path:uri { transport.path = path; } ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; } ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); } shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } tls_name = comma sni:("tls-name") equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); } sni = comma "sni" equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); } flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); } public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); } short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; } block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; } idle_session_check_interval = comma "idle-session-check-interval" equals match:$[0-9]+ { proxy["idle-session-check-interval"] = parseInt(match.trim()); } idle_session_timeout = comma "idle-session-timeout" equals match:$[0-9]+ { proxy["idle-session-timeout"] = parseInt(match.trim()); } min_idle_session = comma "min-idle-session" equals match:$[0-9]+ { proxy["min-idle-session"] = parseInt(match.trim()); } max_stream_count = comma "max-stream-count" equals match:$[0-9]+ { proxy["max-stream-count"] = parseInt(match.trim()); } udp_over_tcp = comma "udp-over-tcp" equals flag:bool { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } tag = match:[^=,]* { proxy.name = match.join("").trim(); } comma = _ "," _ equals = _ "=" _ _ = [ \r\t]* bool = b:("true"/"false") { return b === "true" } others = comma [^=,]+ equals [^=,]+ ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/qx.js ================================================ import * as peggy from 'peggy'; const grammars = String.raw` // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } }} // per-parse initializer { const proxy = {}; const obfs = {}; const $ = {}; function handleObfs() { if (obfs.type === "ws" || obfs.type === "wss") { proxy.network = "ws"; if (obfs.type === 'wss') { proxy.tls = true; } $set(proxy, "ws-opts.path", obfs.path); $set(proxy, "ws-opts.headers.Host", obfs.host); } else if (obfs.type === "over-tls") { proxy.tls = true; } else if (obfs.type === "http") { proxy.network = "http"; $set(proxy, "http-opts.path", obfs.path); $set(proxy, "http-opts.headers.Host", obfs.host); } } } start = (trojan/shadowsocks/vmess/vless/http/socks5) { return proxy } trojan = "trojan" equals address (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { proxy.type = "trojan"; handleObfs(); } shadowsocks = "shadowsocks" equals address (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { if (proxy.protocol || proxy.type === "ssr") { proxy.type = "ssr"; if (!proxy.protocol) { proxy.protocol = "origin"; } // handle ssr obfs if (obfs.host) proxy["obfs-param"] = obfs.host; if (obfs.type) proxy.obfs = obfs.type; } else { proxy.type = "ss"; // handle ss obfs if (obfs.type == "http" || obfs.type === "tls") { proxy.plugin = "obfs"; $set(proxy, "plugin-opts", { mode: obfs.type }); } else if (obfs.type === "ws" || obfs.type === "wss") { proxy.plugin = "v2ray-plugin"; $set(proxy, "plugin-opts.mode", "websocket"); if (obfs.type === "wss") { $set(proxy, "plugin-opts.tls", true); } } else if (obfs.type === 'over-tls') { throw new Error('ss over-tls is not supported'); } if (obfs.type) { $set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.path", obfs.path); } } } vmess = "vmess" equals address (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; if (proxy.aead === false) { proxy.alterId = 1; } else { proxy.alterId = 0; } handleObfs(); } vless = "vless" equals address (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/vless_flow/others)* { proxy.type = "vless"; proxy.cipher = proxy.cipher || "none"; handleObfs(); } http = "http" equals address (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)*{ proxy.type = "http"; } socks5 = "socks5" equals address (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { proxy.type = "socks5"; } address = server:server ":" port:port { proxy.server = server; proxy.port = port; } server = ip/domain domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } } ip = & { const start = peg$currPos; let end; let j = start; while (j < input.length) { if (input[j] === ",") break; if (input[j] === ":") end = j; j++; } peg$currPos = end || j; $.ip = input.substring(start, end).trim(); return true; } { return $.ip; } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } } username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); } password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); } uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); } method = comma "method" equals cipher:cipher { proxy.cipher = cipher; }; cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm"); aead = comma "aead" equals flag:bool { proxy.aead = flag; } udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); } udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } tls_host = comma sni:("tls-host") equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); } tls_verification = comma "tls-verification" equals flag:bool { proxy["skip-cert-verify"] = !flag; } tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; } tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; } tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool { proxy["tls-no-session-ticket"] = flag; } tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool { proxy["tls-no-session-reuse"] = flag; } obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; } obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; }; obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); } obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; } ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } reality_base64_pubkey = comma "reality-base64-pubkey" equals param:$[^=,]+ { $set(proxy, "reality-opts.public-key", param); } reality_hex_shortid = comma "reality-hex-shortid" equals param:$[^=,]+ { $set(proxy, "reality-opts.short-id", param); } vless_flow = comma "vless-flow" equals param:$[^=,]+ { proxy["flow"] = param; } server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; } uri = $[^,]+ tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } others = comma [^=,]+ equals [^=,]+ comma = _ "," _ equals = _ "=" _ _ = [ \r\t]* bool = b:("true"/"false") { return b === "true" } `; let parser; export default function getParser() { if (!parser) { parser = peggy.generate(grammars); } return parser; } ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/qx.peg ================================================ // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } }} // per-parse initializer { const proxy = {}; const obfs = {}; const $ = {}; function handleObfs() { if (obfs.type === "ws" || obfs.type === "wss") { proxy.network = "ws"; if (obfs.type === 'wss') { proxy.tls = true; } $set(proxy, "ws-opts.path", obfs.path); $set(proxy, "ws-opts.headers.Host", obfs.host); } else if (obfs.type === "over-tls") { proxy.tls = true; } else if (obfs.type === "http") { proxy.network = "http"; $set(proxy, "http-opts.path", obfs.path); $set(proxy, "http-opts.headers.Host", obfs.host); } } } start = (trojan/shadowsocks/vmess/vless/http/socks5) { return proxy } trojan = "trojan" equals address (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { proxy.type = "trojan"; handleObfs(); } shadowsocks = "shadowsocks" equals address (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { if (proxy.protocol || proxy.type === "ssr") { proxy.type = "ssr"; if (!proxy.protocol) { proxy.protocol = "origin"; } // handle ssr obfs if (obfs.host) proxy["obfs-param"] = obfs.host; if (obfs.type) proxy.obfs = obfs.type; } else { proxy.type = "ss"; // handle ss obfs if (obfs.type == "http" || obfs.type === "tls") { proxy.plugin = "obfs"; $set(proxy, "plugin-opts", { mode: obfs.type }); } else if (obfs.type === "ws" || obfs.type === "wss") { proxy.plugin = "v2ray-plugin"; $set(proxy, "plugin-opts.mode", "websocket"); if (obfs.type === "wss") { $set(proxy, "plugin-opts.tls", true); } } else if (obfs.type === 'over-tls') { throw new Error('ss over-tls is not supported'); } if (obfs.type) { $set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.path", obfs.path); } } } vmess = "vmess" equals address (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; if (proxy.aead === false) { proxy.alterId = 1; } else { proxy.alterId = 0; } handleObfs(); } vless = "vless" equals address (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/reality_base64_pubkey/reality_hex_shortid/vless_flow/others)* { proxy.type = "vless"; proxy.cipher = proxy.cipher || "none"; handleObfs(); } http = "http" equals address (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)*{ proxy.type = "http"; } socks5 = "socks5" equals address (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* { proxy.type = "socks5"; } address = server:server ":" port:port { proxy.server = server; proxy.port = port; } server = ip/domain domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } } ip = & { const start = peg$currPos; let end; let j = start; while (j < input.length) { if (input[j] === ",") break; if (input[j] === ":") end = j; j++; } peg$currPos = end || j; $.ip = input.substring(start, end).trim(); return true; } { return $.ip; } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } } username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); } password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); } uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); } method = comma "method" equals cipher:cipher { proxy.cipher = cipher; }; cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm"); aead = comma "aead" equals flag:bool { proxy.aead = flag; } udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); } udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } tls_host = comma sni:("tls-host") equals match:[^,]+ { proxy.sni = match.join("").replace(/^"(.*)"$/, '$1'); } tls_verification = comma "tls-verification" equals flag:bool { proxy["skip-cert-verify"] = !flag; } tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; } tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; } tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool { proxy["tls-no-session-ticket"] = flag; } tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool { proxy["tls-no-session-reuse"] = flag; } obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; } obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; }; obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); } obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; } ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; } ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } reality_base64_pubkey = comma "reality-base64-pubkey" equals param:$[^=,]+ { $set(proxy, "reality-opts.public-key", param); } reality_hex_shortid = comma "reality-hex-shortid" equals param:$[^=,]+ { $set(proxy, "reality-opts.short-id", param); } vless_flow = comma "vless-flow" equals param:$[^=,]+ { proxy["flow"] = param; } server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; } uri = $[^,]+ tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } others = comma [^=,]+ equals [^=,]+ comma = _ "," _ equals = _ "=" _ _ = [ \r\t]* bool = b:("true"/"false") { return b === "true" } ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/surge.js ================================================ import * as peggy from 'peggy'; const grammars = String.raw` // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } }} // per-parser initializer { const proxy = {}; const obfs = {}; const $ = {}; function handleWebsocket() { if (obfs.type === "ws") { proxy.network = "ws"; $set(proxy, "ws-opts.path", obfs.path); $set(proxy, "ws-opts.headers", obfs['ws-headers']); if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) { proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1') } } } function handleShadowTLS() { if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) { proxy['shadow-tls-version'] = 2; } } } start = (anytls/shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/trust_tunnel/direct) { return proxy; } shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* { proxy.type = "ss"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { proxy.plugin = "obfs"; $set(proxy, "plugin-opts.mode", obfs.type); $set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.path", obfs.path); } handleShadowTLS(); } vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; // Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess if (proxy.aead) { proxy.alterId = 0; } else { proxy.alterId = 1; } handleWebsocket(); handleShadowTLS(); } trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "trojan"; handleWebsocket(); handleShadowTLS(); } https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; proxy.tls = true; handleShadowTLS(); } http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; handleShadowTLS(); } ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "ssh"; handleShadowTLS(); } snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "snell"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { $set(proxy, "obfs-opts.mode", obfs.type); $set(proxy, "obfs-opts.host", obfs.host); $set(proxy, "obfs-opts.path", obfs.path); } handleShadowTLS(); } tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* { proxy.type = "tuic"; handleShadowTLS(); } tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* { proxy.type = "tuic"; proxy.version = 5; handleShadowTLS(); } wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "wireguard-surge"; handleShadowTLS(); } hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/others)* { proxy.type = "hysteria2"; handleShadowTLS(); } socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; handleShadowTLS(); } socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; proxy.tls = true; handleShadowTLS(); } anytls = tag equals "anytls" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* { proxy.type = "anytls"; proxy.tls = true; } trust_tunnel = tag equals "trust-tunnel" address (usernamek/passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* { proxy.type = "trusttunnel"; proxy.tls = true; } direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* { proxy.type = "direct"; } address = comma server:server comma port:port { proxy.server = server; proxy.port = port; } server = ip/domain ip = & { const start = peg$currPos; let j = start; while (j < input.length) { if (input[j] === ",") break; j++; } peg$currPos = j; $.ip = input.substring(start, j).trim(); return true; } { return $.ip; } domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } } port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); } username = & { let j = peg$currPos; let start, end; let first = true; while (j < input.length) { if (input[j] === ',') { if (first) { start = j + 1; first = false; } else { end = j; break; } } j++; } const match = input.substring(start, end); if (match.indexOf("=") === -1) { $.username = match; peg$currPos = end; return true; } } { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } tls = comma "tls" equals flag:bool { proxy.tls = flag; } sni = comma "sni" equals match:[^,]+ { const sni = match.join("").replace(/^"(.*)"$/, '$1'); if (sni === "off") { proxy["disable-sni"] = true; } else { proxy.sni = sni; } } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } method = comma "encrypt-method" equals cipher:cipher { proxy.cipher = cipher; } cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm"); ws = comma "ws" equals flag:bool { obfs.type = "ws"; } ws_headers = comma "ws-headers" equals headers:$[^,]+ { const pairs = headers.split("|"); const result = {}; pairs.forEach(pair => { const [key, value] = pair.trim().split(":"); result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }) obfs["ws-headers"] = result; } ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; } obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); }; obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path } uri = $[^,]+ udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); } no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); } underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); } test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); } test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); } tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); } interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); } allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; } hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; } idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); } private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); } server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); } udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); } shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'salamander'; } tag = match:[^=,]* { proxy.name = match.join("").trim(); } comma = _ "," _ equals = _ "=" _ _ = [ \r\t]* bool = b:("true"/"false") { return b === "true" } others = comma [^=,]+ equals [^=,]+ `; let parser; export default function getParser() { if (!parser) { parser = peggy.generate(grammars); } return parser; } ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/surge.peg ================================================ // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } }} // per-parser initializer { const proxy = {}; const obfs = {}; const $ = {}; function handleWebsocket() { if (obfs.type === "ws") { proxy.network = "ws"; $set(proxy, "ws-opts.path", obfs.path); $set(proxy, "ws-opts.headers", obfs['ws-headers']); if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) { proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1') } } } function handleShadowTLS() { if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) { proxy['shadow-tls-version'] = 2; } } } start = (anytls/shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/trust_tunnel/direct) { return proxy; } shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* { proxy.type = "ss"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { proxy.plugin = "obfs"; $set(proxy, "plugin-opts.mode", obfs.type); $set(proxy, "plugin-opts.host", obfs.host); $set(proxy, "plugin-opts.path", obfs.path); } handleShadowTLS(); } vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "vmess"; proxy.cipher = proxy.cipher || "none"; // Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess if (proxy.aead) { proxy.alterId = 0; } else { proxy.alterId = 1; } handleWebsocket(); handleShadowTLS(); } trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "trojan"; handleWebsocket(); handleShadowTLS(); } https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; proxy.tls = true; handleShadowTLS(); } http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "http"; handleShadowTLS(); } ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "ssh"; handleShadowTLS(); } snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "snell"; // handle obfs if (obfs.type == "http" || obfs.type === "tls") { $set(proxy, "obfs-opts.mode", obfs.type); $set(proxy, "obfs-opts.host", obfs.host); $set(proxy, "obfs-opts.path", obfs.path); } handleShadowTLS(); } tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* { proxy.type = "tuic"; handleShadowTLS(); } tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* { proxy.type = "tuic"; proxy.version = 5; handleShadowTLS(); } wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "wireguard-surge"; handleShadowTLS(); } hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/others)* { proxy.type = "hysteria2"; handleShadowTLS(); } socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; handleShadowTLS(); } socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { proxy.type = "socks5"; proxy.tls = true; handleShadowTLS(); } anytls = tag equals "anytls" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* { proxy.type = "anytls"; proxy.tls = true; } trust_tunnel = tag equals "trust-tunnel" address (usernamek/passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/block_quic/others)* { proxy.type = "trusttunnel"; proxy.tls = true; } direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* { proxy.type = "direct"; } address = comma server:server comma port:port { proxy.server = server; proxy.port = port; } server = ip/domain ip = & { const start = peg$currPos; let j = start; while (j < input.length) { if (input[j] === ",") break; j++; } peg$currPos = j; $.ip = input.substring(start, j).trim(); return true; } { return $.ip; } domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } } port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); } username = & { let j = peg$currPos; let start, end; let first = true; while (j < input.length) { if (input[j] === ',') { if (first) { start = j + 1; first = false; } else { end = j; break; } } j++; } const match = input.substring(start, end); if (match.indexOf("=") === -1) { $.username = match; peg$currPos = end; return true; } } { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } tls = comma "tls" equals flag:bool { proxy.tls = flag; } sni = comma "sni" equals match:[^,]+ { const sni = match.join("").replace(/^"(.*)"$/, '$1'); if (sni === "off") { proxy["disable-sni"] = true; } else { proxy.sni = sni; } } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } method = comma "encrypt-method" equals cipher:cipher { proxy.cipher = cipher; } cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm"); ws = comma "ws" equals flag:bool { obfs.type = "ws"; } ws_headers = comma "ws-headers" equals headers:$[^,]+ { const pairs = headers.split("|"); const result = {}; pairs.forEach(pair => { const [key, value] = pair.trim().split(":"); result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }) obfs["ws-headers"] = result; } ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; } obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); }; obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path } uri = $[^,]+ udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; } ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); } section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); } no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); } underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); } test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); } test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); } tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); } interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); } allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; } hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; } idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); } private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); } server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); } udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); } shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'salamander'; } tag = match:[^=,]* { proxy.name = match.join("").trim(); } comma = _ "," _ equals = _ "=" _ _ = [ \r\t]* bool = b:("true"/"false") { return b === "true" } others = comma [^=,]+ equals [^=,]+ ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js ================================================ import * as peggy from 'peggy'; const grammars = String.raw` // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } function toBool(str) { if (typeof str === 'undefined' || str === null) return undefined; return /(TRUE)|1/i.test(str); } }} { const proxy = {}; const obfs = {}; const $ = {}; const params = {}; } start = (trojan) { return proxy } trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{ proxy.type = "trojan"; proxy.password = password; proxy.server = server; proxy.port = port; proxy.name = name; // name may be empty if (!proxy.name) { proxy.name = server + ":" + port; } }; password = match:$[^@]+ { return decodeURIComponent(match); }; server = ip/domain; domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } } ip = & { const start = peg$currPos; let end; let j = start; while (j < input.length) { if (input[j] === ",") break; if (input[j] === ":") end = j; j++; } peg$currPos = end || j; $.ip = input.substring(start, end).trim(); return true; } { return $.ip; } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } else { throw new Error("Invalid port: " + port); } } params = "?" head:param tail:("&"@param)* { for (const [key, value] of Object.entries(params)) { params[key] = decodeURIComponent(value); } proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy.sni = params["sni"] || params["peer"]; proxy['client-fingerprint'] = params.fp; proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined; if (toBool(params["ws"])) { proxy.network = "ws"; $set(proxy, "ws-opts.path", params["wspath"]); } if (params["type"]) { let httpupgrade proxy.network = params["type"] if(proxy.network === 'httpupgrade') { proxy.network = 'ws' httpupgrade = true } if (['grpc'].includes(proxy.network)) { proxy[proxy.network + '-opts'] = { 'grpc-service-name': params["serviceName"], '_grpc-type': params["mode"], '_grpc-authority': params["authority"], }; } else { if (params["path"]) { $set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"])); } if (params["host"]) { $set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"])); } if (httpupgrade) { $set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true); $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true); } } if (['reality'].includes(params.security)) { const opts = {}; if (params.pbk) { opts['public-key'] = params.pbk; } if (params.sid) { opts['short-id'] = params.sid; } if (params.spx) { opts['_spider-x'] = params.spx; } if (params.mode) { proxy._mode = params.mode; } if (params.extra) { proxy._extra = params.extra; } if (Object.keys(opts).length > 0) { $set(proxy, params.security+"-opts", opts); } } } proxy.udp = toBool(params["udp"]); proxy.tfo = toBool(params["tfo"]); } param = kv/single; kv = key:$[a-z]i+ "=" value:$[^&#]i* { params[key] = value; } single = key:$[a-z]i+ { params[key] = true; }; name = "#" + match:$.* { return decodeURIComponent(match); } `; let parser; export default function getParser() { if (!parser) { parser = peggy.generate(grammars); } return parser; } ================================================ FILE: backend/src/core/proxy-utils/parsers/peggy/trojan-uri.peg ================================================ // global initializer {{ function $set(obj, path, value) { if (Object(obj) !== obj) return obj; if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; path .slice(0, -1) .reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[ path[path.length - 1] ] = value; return obj; } function toBool(str) { if (typeof str === 'undefined' || str === null) return undefined; return /(TRUE)|1/i.test(str); } }} { const proxy = {}; const obfs = {}; const $ = {}; const params = {}; } start = (trojan) { return proxy } trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{ proxy.type = "trojan"; proxy.password = password; proxy.server = server; proxy.port = port; proxy.name = name; // name may be empty if (!proxy.name) { proxy.name = server + ":" + port; } }; password = match:$[^@]+ { return decodeURIComponent(match); }; server = ip/domain; domain = match:[0-9a-zA-z-_.]+ { const domain = match.join(""); if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) { return domain; } } ip = & { const start = peg$currPos; let end; let j = start; while (j < input.length) { if (input[j] === ",") break; if (input[j] === ":") end = j; j++; } peg$currPos = end || j; $.ip = input.substring(start, end).trim(); return true; } { return $.ip; } port = digits:[0-9]+ { const port = parseInt(digits.join(""), 10); if (port >= 0 && port <= 65535) { return port; } else { throw new Error("Invalid port: " + port); } } params = "?" head:param tail:("&"@param)* { for (const [key, value] of Object.entries(params)) { params[key] = decodeURIComponent(value); } proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy.sni = params["sni"] || params["peer"]; proxy['client-fingerprint'] = params.fp; proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined; if (toBool(params["ws"])) { proxy.network = "ws"; $set(proxy, "ws-opts.path", params["wspath"]); } if (params["type"]) { let httpupgrade proxy.network = params["type"] if(proxy.network === 'httpupgrade') { proxy.network = 'ws' httpupgrade = true } if (['grpc'].includes(proxy.network)) { proxy[proxy.network + '-opts'] = { 'grpc-service-name': params["serviceName"], '_grpc-type': params["mode"], '_grpc-authority': params["authority"], }; } else { if (params["path"]) { $set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"])); } if (params["host"]) { $set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"])); } if (httpupgrade) { $set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true); $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true); } } if (['reality'].includes(params.security)) { const opts = {}; if (params.pbk) { opts['public-key'] = params.pbk; } if (params.sid) { opts['short-id'] = params.sid; } if (params.spx) { opts['_spider-x'] = params.spx; } if (params.mode) { proxy._mode = params.mode; } if (params.extra) { proxy._extra = params.extra; } if (Object.keys(opts).length > 0) { $set(proxy, params.security+"-opts", opts); } } } proxy.udp = toBool(params["udp"]); proxy.tfo = toBool(params["tfo"]); } param = kv/single; kv = key:$[a-z]i+ "=" value:$[^&#]i* { params[key] = value; } single = key:$[a-z]i+ { params[key] = true; }; name = "#" + match:$.* { return decodeURIComponent(match); } ================================================ FILE: backend/src/core/proxy-utils/preprocessors/index.js ================================================ import { safeLoad } from '@/utils/yaml'; import { Base64 } from 'js-base64'; import $ from '@/core/app'; function HTML() { const name = 'HTML'; const test = (raw) => /^/.test(raw); // simply discard HTML const parse = () => ''; return { name, test, parse }; } function Base64Encoded() { const name = 'Base64 Pre-processor'; const keys = [ 'dm1lc3M', // vmess 'c3NyOi8v', // ssr:// 'c29ja3M6Ly', // socks:// 'dHJvamFu', // trojan 'c3M6Ly', // ss:/ 'c3NkOi8v', // ssd:// 'c2hhZG93', // shadow 'aHR0c', // htt 'dmxlc3M=', // vless 'aHlzdGVyaWEy', // hysteria2 'aHkyOi8v', // hy2:// 'd2lyZWd1YXJkOi8v', // wireguard:// 'd2c6Ly8=', // wg:// 'dHVpYzovLw==', // tuic:// ]; const test = function (raw) { return ( !/^\w+:\/\/\w+/im.test(raw) && keys.some((k) => raw.indexOf(k) !== -1) ); }; const parse = function (raw) { const decoded = Base64.decode(raw); if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) { $.error( `Base64 Pre-processor error: decoded line does not start with protocol`, ); return raw; } return decoded; }; return { name, test, parse }; } function fallbackBase64Encoded() { const name = 'Fallback Base64 Pre-processor'; const test = function (raw) { return true; }; const parse = function (raw) { const decoded = Base64.decode(raw); if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) { $.error( `Fallback Base64 Pre-processor error: decoded line does not start with protocol`, ); return raw; } return decoded; }; return { name, test, parse }; } function Clash() { const name = 'Clash Pre-processor'; const test = function (raw) { if (!/proxies/.test(raw)) return false; const content = safeLoad(raw); return content.proxies && Array.isArray(content.proxies); }; const parse = function (raw, includeProxies) { // Clash YAML format // 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity // 匹配 short-id 冒号后面的值(包含空格和引号) const afterReplace = raw.replace( /short-id:([ \t]*[^#\n,}]*)/g, (matched, value) => { const afterTrim = value.trim(); // 为空 if (!afterTrim || afterTrim === '') { return 'short-id: ""'; } // 是否被引号包裹 if (/^(['"]).*\1$/.test(afterTrim)) { return `short-id: ${afterTrim}`; } else if (['null'].includes(afterTrim)) { return `short-id: ${afterTrim}`; } else { return `short-id: "${afterTrim}"`; } }, ); const { proxies } = safeLoad(afterReplace); return ( (includeProxies ? 'proxies:\n' : '') + proxies .map((p) => { return `${includeProxies ? ' - ' : ''}${JSON.stringify( p, )}\n`; }) .join('') ); }; return { name, test, parse }; } function SSD() { const name = 'SSD Pre-processor'; const test = function (raw) { return raw.indexOf('ssd://') === 0; }; const parse = function (raw) { // preprocessing for SSD subscription format const output = []; let ssdinfo = JSON.parse(Base64.decode(raw.split('ssd://')[1])); let port = ssdinfo.port; let method = ssdinfo.encryption; let password = ssdinfo.password; // servers config let servers = ssdinfo.servers; for (let i = 0; i < servers.length; i++) { let server = servers[i]; method = server.encryption ? server.encryption : method; password = server.password ? server.password : password; let userinfo = Base64.encode(method + ':' + password); let hostname = server.server; port = server.port ? server.port : port; let tag = server.remarks ? server.remarks : i; let plugin = server.plugin_options ? '/?plugin=' + encodeURIComponent( server.plugin + ';' + server.plugin_options, ) : ''; output[i] = 'ss://' + userinfo + '@' + hostname + ':' + port + plugin + '#' + tag; } return output.join('\n'); }; return { name, test, parse }; } function FullConfig() { const name = 'Full Config Preprocessor'; const test = function (raw) { return /^(\[server_local\]|\[Proxy\])/gm.test(raw); }; const parse = function (raw) { const match = raw.match( /^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im, )?.[1]; return match || raw; }; return { name, test, parse }; } export default [ HTML(), Clash(), Base64Encoded(), SSD(), FullConfig(), fallbackBase64Encoded(), ]; ================================================ FILE: backend/src/core/proxy-utils/processors/index.js ================================================ import resourceCache from '@/utils/resource-cache'; import scriptResourceCache from '@/utils/script-resource-cache'; import { isIPv4, isIPv6, ipAddress } from '@/utils'; import { FULL } from '@/utils/logical'; import { getFlag, removeFlag } from '@/utils/geo'; import { doh } from '@/utils/dns'; import lodash from 'lodash'; import $ from '@/core/app'; import { hex_md5 } from '@/vendor/md5'; import { ProxyUtils } from '@/core/proxy-utils'; import { produceArtifact } from '@/restful/sync'; import { SETTINGS_KEY } from '@/constants'; import YAML from '@/utils/yaml'; import env from '@/utils/env'; import { getFlowField, getFlowHeaders, parseFlowHeaders, validCheck, flowTransfer, getRmainingDays, normalizeFlowHeader, } from '@/utils/flow'; function isObject(item) { return item && typeof item === 'object' && !Array.isArray(item); } function trimWrap(str) { if (str.startsWith('<') && str.endsWith('>')) { return str.slice(1, -1); } return str; } function deepMerge(target, _other) { const other = typeof _other === 'string' ? JSON.parse(_other) : _other; for (const key in other) { if (isObject(other[key])) { if (key.endsWith('!')) { const k = trimWrap(key.slice(0, -1)); target[k] = other[key]; } else { const k = trimWrap(key); if (!target[k]) Object.assign(target, { [k]: {} }); deepMerge(target[k], other[k]); } } else if (Array.isArray(other[key])) { if (key.startsWith('+')) { const k = trimWrap(key.slice(1)); if (!target[k]) Object.assign(target, { [k]: [] }); target[k] = [...other[key], ...target[k]]; } else if (key.endsWith('+')) { const k = trimWrap(key.slice(0, -1)); if (!target[k]) Object.assign(target, { [k]: [] }); target[k] = [...target[k], ...other[key]]; } else { const k = trimWrap(key); Object.assign(target, { [k]: other[key] }); } } else { Object.assign(target, { [key]: other[key] }); } } return target; } /** The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows: { operator: "AND", child: [ { attr: "name", proposition: "CONTAINS", value: "🇨🇳" }, { attr: "port", proposition: "IN", value: [80, 443] } ] } */ function ConditionalFilter({ rule }) { return { name: 'Conditional Filter', func: (proxies) => { return proxies.map((proxy) => isMatch(rule, proxy)); }, }; } function isMatch(rule, proxy) { // leaf node if (!rule.operator) { switch (rule.proposition) { case 'IN': return rule.value.indexOf(proxy[rule.attr]) !== -1; case 'CONTAINS': if (typeof proxy[rule.attr] !== 'string') return false; return proxy[rule.attr].indexOf(rule.value) !== -1; case 'EQUALS': return proxy[rule.attr] === rule.value; case 'EXISTS': return ( proxy[rule.attr] !== null || typeof proxy[rule.attr] !== 'undefined' ); default: throw new Error(`Unknown proposition: ${rule.proposition}`); } } // operator nodes switch (rule.operator) { case 'AND': return rule.child.every((child) => isMatch(child, proxy)); case 'OR': return rule.child.some((child) => isMatch(child, proxy)); case 'NOT': return !isMatch(rule.child, proxy); default: throw new Error(`Unknown operator: ${rule.operator}`); } } function QuickSettingOperator(args) { return { name: 'Quick Setting Operator', func: (proxies) => { if (get(args.useless)) { const filter = UselessFilter(); const selected = filter.func(proxies); proxies = proxies.filter( (p, i) => selected[i] && p.port > 0 && p.port <= 65535, ); } return proxies.map((proxy) => { proxy.udp = get(args.udp, proxy.udp); proxy.tfo = get(args.tfo, proxy.tfo); proxy['fast-open'] = get(args.tfo, proxy['fast-open']); proxy['skip-cert-verify'] = get( args.scert, proxy['skip-cert-verify'], ); if (proxy.type === 'vmess') { proxy.aead = get(args['vmess aead'], proxy.aead); } return proxy; }); }, }; function get(value, defaultValue) { switch (value) { case 'ENABLED': return true; case 'DISABLED': return false; default: return defaultValue; } } } // add or remove flag for proxies function FlagOperator({ mode, tw }) { return { name: 'Flag Operator', func: (proxies) => { return proxies.map((proxy) => { if (mode === 'remove') { // no flag proxy.name = removeFlag(proxy.name); } else { // get flag const newFlag = getFlag(proxy.name); // remove old flag proxy.name = removeFlag(proxy.name); proxy.name = newFlag + ' ' + proxy.name; if (tw == 'ws') { proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸'); } else if (tw == 'tw') { // 不变 } else { proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳'); } } return proxy; }); }, }; } // duplicate handler function HandleDuplicateOperator(arg) { const { action, template, link, position, field } = { ...{ action: 'rename', template: '0 1 2 3 4 5 6 7 8 9', link: '-', position: 'back', field: ['name'], }, ...arg, }; return { name: 'Handle Duplicate Operator', func: (proxies) => { if (action === 'delete') { const chosen = {}; return proxies.filter((p) => { const key = field .map((f) => lodash.get(p, f, '-')) .join('_'); if (chosen[key]) { return false; } chosen[key] = true; return true; }); } else if (action === 'rename') { const numbers = template.split(' '); // count occurrences of each name const counter = {}; let maxLen = 0; proxies.forEach((p) => { const key = field .map((f) => lodash.get(p, f, '-')) .join('_'); if (typeof counter[key] === 'undefined') counter[key] = 1; else counter[key]++; maxLen = Math.max(counter[key].toString().length, maxLen); }); const increment = {}; return proxies.map((p) => { const key = field .map((f) => lodash.get(p, f, '-')) .join('_'); if (counter[key] > 1) { if (typeof increment[key] == 'undefined') increment[key] = 1; let num = ''; let cnt = increment[key]++; let numDigits = 0; while (cnt > 0) { num = numbers[cnt % 10] + num; cnt = parseInt(cnt / 10); numDigits++; } // padding while (numDigits++ < maxLen) { num = numbers[0] + num; } if (position === 'front') { p.name = num + link + p.name; } else if (position === 'back') { p.name = p.name + link + num; } } return p; }); } }, }; } // sort proxies according to their names function SortOperator(order = 'asc') { return { name: 'Sort Operator', func: (proxies) => { switch (order) { case 'asc': case 'desc': return proxies.sort((a, b) => { let res = a.name > b.name ? 1 : -1; res *= order === 'desc' ? -1 : 1; return res; }); case 'random': return shuffle(proxies); default: throw new Error('Unknown sort option: ' + order); } }, }; } // sort by regex function RegexSortOperator(input) { const order = input.order || 'asc'; let expressions = input.expressions; if (Array.isArray(input)) { expressions = input; } if (!Array.isArray(expressions)) { expressions = []; } return { name: 'Regex Sort Operator', func: (proxies) => { expressions = expressions.map((expr) => buildRegex(expr)); return proxies.sort((a, b) => { const oA = getRegexOrder(expressions, a.name); const oB = getRegexOrder(expressions, b.name); if (oA && !oB) return -1; if (oB && !oA) return 1; if (oA && oB) return oA < oB ? -1 : 1; if (order === 'original') { return 0; } else if (order === 'desc') { return a.name < b.name ? 1 : -1; } else { return a.name < b.name ? -1 : 1; } }); }, }; } function getRegexOrder(expressions, str) { let order = null; for (let i = 0; i < expressions.length; i++) { if (expressions[i].test(str)) { order = i + 1; // plus 1 is important! 0 will be treated as false!!! break; } } return order; } // rename by regex // keywords: [{expr: "string format regex", now: "now"}] function RegexRenameOperator(regex) { return { name: 'Regex Rename Operator', func: (proxies) => { return proxies.map((proxy) => { for (const { expr, now } of regex) { proxy.name = proxy.name .replace(buildRegex(expr, 'g'), now) .trim(); } return proxy; }); }, }; } // delete regex operator // regex: ['a', 'b', 'c'] function RegexDeleteOperator(regex) { const regex_ = regex.map((r) => { return { expr: r, now: '', }; }); return { name: 'Regex Delete Operator', func: RegexRenameOperator(regex_).func, }; } /** Script Operator function operator(proxies) { const {arg1} = $arguments; // do something return proxies; } WARNING: 1. This function name should be `operator`! 2. Always declare variables before using them! */ function ScriptOperator( script, targetPlatform, $arguments, source, $options, context, ) { context.source = source; context.env = env; return { name: 'Script Operator', func: async (proxies) => { let output = proxies; if (output?.$file?.type === 'mihomoProfile') { try { let patch = YAML.safeLoad(script); let config; if (output?.$content) { try { config = YAML.safeLoad(output?.$content); } catch (e) { $.error(e.message ?? e); } } // if (typeof patch !== 'object') patch = {}; if (typeof patch !== 'object') throw new Error('patch is not an object'); output.$content = ProxyUtils.yaml.safeDump( deepMerge( config || (output?.$file?.sourceType === 'none' ? {} : { proxies: await produceArtifact({ type: output?.$file?.sourceType || 'collection', name: output?.$file?.sourceName, platform: 'mihomo', produceType: 'internal', produceOpts: { 'delete-underscore-fields': true, }, }), }), patch, ), ); return output; } catch (e) { // console.log(e); } } await (async function () { const operator = createDynamicFunction( 'operator', script, $arguments, $options, ); output = operator(proxies, targetPlatform, context); })(); return output; }, nodeFunc: async (proxies) => { let output = proxies; await (async function () { const operator = createDynamicFunction( 'operator', `async function operator(input = [], targetPlatform, context) { if (input && (input.$files || input.$content)) { let { $content, $files, $options, $file } = input if($file.type === 'mihomoProfile') { ${script} if(typeof main === 'function') { let config; if ($content) { try { config = ProxyUtils.yaml.safeLoad($content); } catch (e) { console.log(e.message ?? e); } } $content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : { proxies: await produceArtifact({ type: $file.sourceType || 'collection', name: $file.sourceName, platform: 'mihomo', produceType: 'internal', produceOpts: { 'delete-underscore-fields': true } }), }))) } } else { ${script} } return { $content, $files, $options, $file } } else { let proxies = input let list = [] for await (let $server of proxies) { ${script} list.push($server) } return list } }`, $arguments, $options, ); output = operator(proxies, targetPlatform, context); })(); return output; }, }; } function parseIP4P(IP4P) { let server; let port; try { let array = IP4P.split(':'); port = parseInt(array[2], 16); let ipab = parseInt(array[3], 16); let ipcd = parseInt(array[4], 16); let ipa = ipab >> 8; let ipb = ipab & 0xff; let ipc = ipcd >> 8; let ipd = ipcd & 0xff; server = `${ipa}.${ipb}.${ipc}.${ipd}`; if (port <= 0 || port > 65535) { throw new Error(`Invalid port number: ${port}`); } if (!isIPv4(server)) { throw new Error(`Invalid IP address: ${server}`); } } catch (e) { // throw new Error(`IP4P 解析失败: ${e}`); $.error(`IP4P 解析失败: ${e}`); } return { server, port }; } const DOMAIN_RESOLVERS = { Custom: async function (domain, type, noCache, timeout, edns, url) { const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`); const cached = resourceCache.get(id); if (!noCache && cached) return cached; const answerType = type === 'IPv6' ? 'AAAA' : 'A'; const res = await doh({ url, domain, type: answerType, timeout, edns, }); const { answers } = res; if (!Array.isArray(answers) || answers.length === 0) { throw new Error('No answers'); } const result = answers .filter((i) => i?.type === answerType) .map((i) => i?.data) .filter((i) => i); if (result.length === 0) { throw new Error('No answers'); } resourceCache.set(id, result); return result; }, Google: async function (domain, type, noCache, timeout, edns) { const id = hex_md5(`GOOGLE:${domain}:${type}`); const cached = resourceCache.get(id); if (!noCache && cached) return cached; const answerType = type === 'IPv6' ? 'AAAA' : 'A'; const res = await doh({ url: 'https://8.8.4.4/dns-query', domain, type: answerType, timeout, edns, }); const { answers } = res; if (!Array.isArray(answers) || answers.length === 0) { throw new Error('No answers'); } const result = answers .filter((i) => i?.type === answerType) .map((i) => i?.data) .filter((i) => i); if (result.length === 0) { throw new Error('No answers'); } resourceCache.set(id, result); return result; }, 'IP-API': async function (domain, type, noCache, timeout) { if (['IPv6'].includes(type)) { throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`); } const id = hex_md5(`IP-API:${domain}`); const cached = resourceCache.get(id); if (!noCache && cached) return cached; const resp = await $.http.get({ url: `http://ip-api.com/json/${encodeURIComponent( domain, )}?lang=zh-CN`, timeout, }); const body = JSON.parse(resp.body); if (body['status'] !== 'success') { throw new Error(`Status is ${body['status']}`); } if (!body.query || body.query === 0) { throw new Error('No answers'); } const result = [body.query]; if (result.length === 0) { throw new Error('No answers'); } resourceCache.set(id, result); return result; }, Cloudflare: async function (domain, type, noCache, timeout, edns) { const id = hex_md5(`CLOUDFLARE:${domain}:${type}`); const cached = resourceCache.get(id); if (!noCache && cached) return cached; const answerType = type === 'IPv6' ? 'AAAA' : 'A'; const res = await doh({ url: 'https://1.0.0.1/dns-query', domain, type: answerType, timeout, edns, }); const { answers } = res; if (!Array.isArray(answers) || answers.length === 0) { throw new Error('No answers'); } const result = answers .filter((i) => i?.type === answerType) .map((i) => i?.data) .filter((i) => i); if (result.length === 0) { throw new Error('No answers'); } resourceCache.set(id, result); return result; }, Ali: async function (domain, type, noCache, timeout, edns) { const id = hex_md5(`ALI:${domain}:${type}`); const cached = resourceCache.get(id); if (!noCache && cached) return cached; const resp = await $.http.get({ url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/${ isIPv4(edns) ? 24 : 56 }&name=${encodeURIComponent(domain)}&type=${ type === 'IPv6' ? 'AAAA' : 'A' }&short=1`, headers: { accept: 'application/dns-json', }, timeout, }); const answers = JSON.parse(resp.body); if (!Array.isArray(answers) || answers.length === 0) { throw new Error('No answers'); } const result = answers; if (result.length === 0) { throw new Error('No answers'); } resourceCache.set(id, result); return result; }, Tencent: async function (domain, type, noCache, timeout, edns) { const id = hex_md5(`TENCENT:${domain}:${type}`); const cached = resourceCache.get(id); if (!noCache && cached) return cached; const resp = await $.http.get({ url: `http://119.28.28.28/d?ip=${edns}&type=${ type === 'IPv6' ? 'AAAA' : 'A' }&dn=${encodeURIComponent(domain)}`, headers: { accept: 'application/dns-json', }, timeout, }); const answers = resp.body.split(';').map((i) => i.split(',')[0]); if (answers.length === 0 || String(answers) === '0') { throw new Error('No answers'); } const result = answers; if (result.length === 0) { throw new Error('No answers'); } resourceCache.set(id, result); return result; }, }; function ResolveDomainOperator({ provider, type: _type, filter, cache, url, timeout, edns: _edns, }) { if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) { throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`); } const { defaultTimeout } = $.read(SETTINGS_KEY); const requestTimeout = timeout || defaultTimeout || 8000; let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4'; const resolver = DOMAIN_RESOLVERS[provider]; if (!resolver) { throw new Error(`找不到域名解析服务提供方: ${provider}`); } let edns = _edns || '223.6.6.6'; if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`); $.info( `Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`, ); return { name: 'Resolve Domain Operator', func: async (proxies) => { proxies.forEach((p, i) => { if (!p['_no-resolve'] && p['no-resolve']) { proxies[i]['_no-resolve'] = p['no-resolve']; } }); const results = {}; const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage. const totalDomain = [ ...new Set( proxies .filter((p) => !isIP(p.server) && !p['_no-resolve']) .map((c) => c.server), ), ]; const totalBatch = Math.ceil(totalDomain.length / limit); for (let i = 0; i < totalBatch; i++) { const currentBatch = []; for (let domain of totalDomain.splice(0, limit)) { currentBatch.push( resolver( domain, type, cache === 'disabled', requestTimeout, edns, url, ) .then((ip) => { results[domain] = ip; $.info( `Successfully resolved domain: ${domain} ➟ ${ip}`, ); }) .catch((err) => { $.error( `Failed to resolve domain: ${domain} with resolver [${provider}]: ${err}`, ); }), ); } await Promise.all(currentBatch); } proxies.forEach((p) => { if (!p['_no-resolve']) { if (results[p.server]) { p._resolved_ips = results[p.server]; let ip = Array.isArray(results[p.server]) ? results[p.server][ Math.floor( Math.random() * results[p.server].length, ) ] : results[p.server]; if (type === 'IPv6' && isIPv6(ip)) { try { ip = new ipAddress.Address6(ip).correctForm(); } catch (e) { $.error( `Failed to parse IPv6 address: ${ip}: ${e}`, ); } if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) { p._IP4P = ip; const { server, port } = parseIP4P(ip); if (server && port) { p._domain = p.server; p.server = server; p.port = port; p.resolved = true; p._IPv4 = p.server; if (!isIP(p._IP)) { p._IP = p.server; } } else if (!p.resolved) { p.resolved = false; } } else { p._domain = p.server; p.server = ip; p.resolved = true; p[`_${type}`] = p.server; if (!isIP(p._IP)) { p._IP = p.server; } } } else { p._domain = p.server; p.server = ip; p.resolved = true; p[`_${type}`] = p.server; if (!isIP(p._IP)) { p._IP = p.server; } } } else if (!p.resolved) { p.resolved = false; } } }); return proxies.filter((p) => { if (filter === 'removeFailed') { return isIP(p.server) || p['_no-resolve'] || p.resolved; } else if (filter === 'IPOnly') { return isIP(p.server); } else if (filter === 'IPv4Only') { return isIPv4(p.server); } else if (filter === 'IPv6Only') { return isIPv6(p.server); } else { return true; } }); }, }; } function isIP(ip) { return isIPv4(ip) || isIPv6(ip); } ResolveDomainOperator.resolver = DOMAIN_RESOLVERS; function isAscii(str) { // eslint-disable-next-line no-control-regex var pattern = /^[\x00-\x7F]+$/; // ASCII 范围的 Unicode 编码 return pattern.test(str); } /**************************** Filters ***************************************/ // filter useless proxies function UselessFilter() { return { name: 'Useless Filter', func: (proxies) => { return proxies.map((proxy) => { if (proxy.cipher && !isAscii(proxy.cipher)) { return false; } else if (proxy.password && !isAscii(proxy.password)) { return false; } else { if (proxy.network) { let transportHosts = proxy[`${proxy.network}-opts`]?.headers?.Host || proxy[`${proxy.network}-opts`]?.headers?.host; transportHosts = Array.isArray(transportHosts) ? transportHosts : [transportHosts]; if ( transportHosts.some( (host) => host && !isAscii(host), ) ) { return false; } } return !/网址|流量|时间|应急|过期|Bandwidth|expire/.test( proxy.name, ); } }); }, }; } // filter by regions function RegionFilter(input) { let regions = input?.value || input; if (!Array.isArray(regions)) { regions = []; } const keep = input?.keep ?? true; const REGION_MAP = { HK: '🇭🇰', TW: '🇹🇼', US: '🇺🇸', SG: '🇸🇬', JP: '🇯🇵', UK: '🇬🇧', DE: '🇩🇪', KR: '🇰🇷', }; return { name: 'Region Filter', func: (proxies) => { // this would be high memory usage return proxies.map((proxy) => { const flag = getFlag(proxy.name); const selected = regions.some((r) => REGION_MAP[r] === flag); return keep ? selected : !selected; }); }, }; } // filter by regex function RegexFilter({ regex = [], keep = true }) { return { name: 'Regex Filter', func: (proxies) => { return proxies.map((proxy) => { const selected = regex.some((r) => { return buildRegex(r).test(proxy.name); }); return keep ? selected : !selected; }); }, }; } function buildRegex(str, ...options) { options = options.join(''); if (str.startsWith('(?i)')) { str = str.substring(4); return new RegExp(str, 'i' + options); } else { return new RegExp(str, options); } } // filter by proxy types function TypeFilter(input) { let types = input?.value || input; if (!Array.isArray(types)) { types = []; } const keep = input?.keep ?? true; return { name: 'Type Filter', func: (proxies) => { return proxies.map((proxy) => { const selected = types.some((t) => proxy.type === t); return keep ? selected : !selected; }); }, }; } /** Script Example function filter(proxies) { return proxies.map(p => { return p.name.indexOf('🇭🇰') !== -1; }); } WARNING: 1. This function name should be `filter`! 2. Always declare variables before using them! */ function ScriptFilter( script, targetPlatform, $arguments, source, $options, context, ) { context.source = source; context.env = env; return { name: 'Script Filter', func: async (proxies) => { let output = FULL(proxies.length, true); await (async function () { const filter = createDynamicFunction( 'filter', script, $arguments, $options, ); output = filter(proxies, targetPlatform, context); })(); return output; }, nodeFunc: async (proxies) => { let output = FULL(proxies.length, true); await (async function () { const filter = createDynamicFunction( 'filter', `async function filter(input = [], targetPlatform, context) { let proxies = input let list = [] const fn = async ($server) => { ${script} } for await (let $server of proxies) { list.push(await fn($server)) } return list }`, $arguments, $options, ); output = filter(proxies, targetPlatform, context); })(); return output; }, }; } export default { 'Useless Filter': UselessFilter, 'Region Filter': RegionFilter, 'Regex Filter': RegexFilter, 'Type Filter': TypeFilter, 'Script Filter': ScriptFilter, 'Conditional Filter': ConditionalFilter, 'Quick Setting Operator': QuickSettingOperator, 'Flag Operator': FlagOperator, 'Sort Operator': SortOperator, 'Regex Sort Operator': RegexSortOperator, 'Regex Rename Operator': RegexRenameOperator, 'Regex Delete Operator': RegexDeleteOperator, 'Script Operator': ScriptOperator, 'Handle Duplicate Operator': HandleDuplicateOperator, 'Resolve Domain Operator': ResolveDomainOperator, }; async function ApplyFilter(filter, objs) { // select proxies let selected = FULL(objs.length, true); try { selected = await filter.func(objs); } catch (err) { let funcErr = ''; let funcErrMsg = `${err.message ?? err}`; if (funcErrMsg.includes('$server is not defined')) { funcErr = ''; } else { $.error( `Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`, ); funcErr = `执行 function filter 失败 ${funcErrMsg}; `; } try { selected = await filter.nodeFunc(objs); } catch (err) { $.error( `Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`, ); let nodeErr = ''; let nodeErrMsg = `${err.message ?? err}`; if (funcErr && nodeErrMsg === funcErrMsg) { nodeErr = ''; funcErr = `执行失败 ${funcErrMsg}`; } else { nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`; } throw new Error(`脚本过滤 ${funcErr}${nodeErr}`); } } return objs.filter((_, i) => selected[i]); } async function ApplyOperator(operator, objs) { let output = clone(objs); try { const output_ = await operator.func(output); if (output_) output = output_; } catch (err) { let funcErr = ''; let funcErrMsg = `${err.message ?? err}`; if ( funcErrMsg.includes('$server is not defined') || funcErrMsg.includes('$content is not defined') || funcErrMsg.includes('$files is not defined') || output?.$files || output?.$content ) { funcErr = ''; } else { $.error( `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`, ); funcErr = `执行 function operator 失败 ${funcErrMsg}; `; } try { const output_ = await operator.nodeFunc(output); if (output_) output = output_; } catch (err) { $.error( `Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`, ); let nodeErr = ''; let nodeErrMsg = `${err.message ?? err}`; if (funcErr && nodeErrMsg === funcErrMsg) { nodeErr = ''; funcErr = `执行失败 ${funcErrMsg}`; } else { nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`; } throw new Error(`脚本操作 ${funcErr}${nodeErr}`); } } return output; } export async function ApplyProcessor(processor, objs) { if (processor.name.indexOf('Filter') !== -1) { return ApplyFilter(processor, objs); } else if (processor.name.indexOf('Operator') !== -1) { return ApplyOperator(processor, objs); } } // shuffle array function shuffle(array) { let currentIndex = array.length, temporaryValue, randomIndex; // While there remain elements to shuffle... while (0 !== currentIndex) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } // deep clone object function clone(object) { return JSON.parse(JSON.stringify(object)); } function createDynamicFunction(name, script, $arguments, $options) { const flowUtils = { getFlowField, getFlowHeaders, parseFlowHeaders, flowTransfer, validCheck, getRmainingDays, normalizeFlowHeader, }; if ($.env.isLoon) { return new Function( '$arguments', '$options', '$substore', 'lodash', '$persistentStore', '$httpClient', '$notification', 'ProxyUtils', 'yaml', 'Buffer', 'b64d', 'b64e', 'scriptResourceCache', 'flowUtils', 'produceArtifact', 'require', `${script}\n return ${name}`, )( $arguments, $options, $, lodash, // eslint-disable-next-line no-undef $persistentStore, // eslint-disable-next-line no-undef $httpClient, // eslint-disable-next-line no-undef $notification, ProxyUtils, ProxyUtils.yaml, ProxyUtils.Buffer, ProxyUtils.Base64.decode, ProxyUtils.Base64.encode, scriptResourceCache, flowUtils, produceArtifact, eval(`typeof require !== "undefined"`) ? require : undefined, ); } else { return new Function( '$arguments', '$options', '$substore', 'lodash', 'ProxyUtils', 'yaml', 'Buffer', 'b64d', 'b64e', 'scriptResourceCache', 'flowUtils', 'produceArtifact', 'require', `${script}\n return ${name}`, )( $arguments, $options, $, lodash, ProxyUtils, ProxyUtils.yaml, ProxyUtils.Buffer, ProxyUtils.Base64.decode, ProxyUtils.Base64.encode, scriptResourceCache, flowUtils, produceArtifact, eval(`typeof require !== "undefined"`) ? require : undefined, ); } } ================================================ FILE: backend/src/core/proxy-utils/producers/clash.js ================================================ import { isPresent } from '@/core/proxy-utils/producers/utils'; import $ from '@/core/app'; export default function Clash_Producer() { const type = 'ALL'; const produce = (proxies, type, opts = {}) => { // VLESS XTLS is not supported by Clash // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532 // github.com/Dreamacro/clash/pull/2891/files // filter unsupported proxies // https://clash.wiki/configuration/outbound.html#shadowsocks const list = proxies .filter((proxy) => { if (opts['include-unsupported-proxy']) return true; if ( ![ 'ss', 'ssr', 'vmess', 'vless', 'socks5', 'http', 'snell', 'trojan', 'wireguard', ].includes(proxy.type) || (proxy.type === 'ss' && ![ 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'rc4-md5', 'chacha20-ietf', 'xchacha20', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', ].includes(proxy.cipher)) || (proxy.type === 'snell' && proxy.version >= 4) || (proxy.type === 'vless' && (typeof proxy.flow !== 'undefined' || proxy['reality-opts'])) ) { return false; } else if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { return false; } else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) { $.error( `Clash 不支持前置代理字段. 已过滤节点 ${proxy.name}`, ); return false; } return true; }) .map((proxy) => { if (proxy.type === 'vmess') { // handle vmess aead if (isPresent(proxy, 'aead')) { if (proxy.aead) { proxy.alterId = 0; } delete proxy.aead; } if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } // https://dreamacro.github.io/clash/configuration/outbound.html#vmess if ( isPresent(proxy, 'cipher') && ![ 'auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', ].includes(proxy.cipher) ) { proxy.cipher = 'auto'; } } else if (proxy.type === 'wireguard') { proxy.keepalive = proxy.keepalive ?? proxy['persistent-keepalive']; proxy['persistent-keepalive'] = proxy.keepalive; proxy['preshared-key'] = proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['pre-shared-key'] = proxy['preshared-key']; } else if (proxy.type === 'snell' && proxy.version < 3) { delete proxy.udp; } else if (proxy.type === 'vless') { if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http' ) { let httpPath = proxy['http-opts']?.path; if ( isPresent(proxy, 'http-opts.path') && !Array.isArray(httpPath) ) { proxy['http-opts'].path = [httpPath]; } let httpHost = proxy['http-opts']?.headers?.Host; if ( isPresent(proxy, 'http-opts.headers.Host') && !Array.isArray(httpHost) ) { proxy['http-opts'].headers.Host = [httpHost]; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'h2' ) { let path = proxy['h2-opts']?.path; if ( isPresent(proxy, 'h2-opts.path') && Array.isArray(path) ) { proxy['h2-opts'].path = path[0]; } let host = proxy['h2-opts']?.headers?.host; if ( isPresent(proxy, 'h2-opts.headers.Host') && !Array.isArray(host) ) { proxy['h2-opts'].headers.host = [host]; } } if (['ws'].includes(proxy.network)) { const networkPath = proxy[`${proxy.network}-opts`]?.path; if (networkPath) { const reg = /^(.*?)(?:\?ed=(\d+))?$/; // eslint-disable-next-line no-unused-vars const [_, path = '', ed = ''] = reg.exec(networkPath); proxy[`${proxy.network}-opts`].path = path; if (ed !== '') { proxy['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol'; proxy['ws-opts']['max-early-data'] = parseInt( ed, 10, ); } } else { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].path = '/'; } } if (proxy['plugin-opts']?.tls) { if (isPresent(proxy, 'skip-cert-verify')) { proxy['plugin-opts']['skip-cert-verify'] = proxy['skip-cert-verify']; } } if ( [ 'trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity', 'anytls', 'trusttunnel', 'naive', ].includes(proxy.type) ) { delete proxy.tls; } if (proxy['tls-fingerprint']) { proxy.fingerprint = proxy['tls-fingerprint']; } delete proxy['tls-fingerprint']; if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { delete proxy.tls; } delete proxy.subName; delete proxy.collectionName; delete proxy.id; delete proxy.resolved; delete proxy['no-resolve']; if (type !== 'internal') { for (const key in proxy) { if (proxy[key] == null || /^_/i.test(key)) { delete proxy[key]; } } } if ( ['grpc'].includes(proxy.network) && proxy[`${proxy.network}-opts`] ) { delete proxy[`${proxy.network}-opts`]['_grpc-type']; delete proxy[`${proxy.network}-opts`]['_grpc-authority']; } return proxy; }); return type === 'internal' ? list : 'proxies:\n' + list .map((proxy) => ' - ' + JSON.stringify(proxy) + '\n') .join(''); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/clashmeta.js ================================================ import { isPresent } from '@/core/proxy-utils/producers/utils'; const ipVersions = { dual: 'dual', 'v4-only': 'ipv4', 'v6-only': 'ipv6', 'prefer-v4': 'ipv4-prefer', 'prefer-v6': 'ipv6-prefer', }; export default function ClashMeta_Producer() { const type = 'ALL'; const produce = (proxies, type, opts = {}) => { const list = proxies .filter((proxy) => { if (opts['include-unsupported-proxy']) return true; if (proxy.type === 'snell' && proxy.version >= 4) { return false; } else if (['juicity', 'naive'].includes(proxy.type)) { return false; } else if ( ['ss'].includes(proxy.type) && ![ 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'aes-128-ccm', 'aes-192-ccm', 'aes-256-ccm', 'aes-128-gcm-siv', 'aes-256-gcm-siv', 'chacha20-ietf', 'chacha20', 'xchacha20', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', 'chacha8-ietf-poly1305', 'xchacha8-ietf-poly1305', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305', 'lea-128-gcm', 'lea-192-gcm', 'lea-256-gcm', 'rabbit128-poly1305', 'aegis-128l', 'aegis-256', 'aez-384', 'deoxys-ii-256-128', 'rc4-md5', 'none', ].includes(proxy.cipher) ) { // https://wiki.metacubex.one/config/proxies/ss/#cipher return false; } else if ( ['anytls'].includes(proxy.type) && proxy.network && (!['tcp'].includes(proxy.network) || (['tcp'].includes(proxy.network) && proxy['reality-opts'])) ) { return false; } else if (['xhttp'].includes(proxy.network)) { return false; } return true; }) .map((proxy) => { if (proxy['reality-opts'] && !proxy['client-fingerprint']) { proxy['client-fingerprint'] = 'chrome'; } if (proxy.type === 'vmess') { // handle vmess aead if (isPresent(proxy, 'aead')) { if (proxy.aead) { proxy.alterId = 0; } delete proxy.aead; } if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400 // https://stash.wiki/proxy-protocols/proxy-types#vmess if ( isPresent(proxy, 'cipher') && ![ 'auto', 'none', 'zero', 'aes-128-gcm', 'chacha20-poly1305', ].includes(proxy.cipher) ) { proxy.cipher = 'auto'; } } else if (proxy.type === 'tuic') { if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } // else { // proxy.alpn = ['h3']; // } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; } // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 if ( (!proxy.token || proxy.token.length === 0) && !isPresent(proxy, 'version') ) { proxy.version = 5; } } else if (proxy.type === 'hysteria') { // auth_str 将会在未来某个时候删除 但是有的机场不规范 if ( isPresent(proxy, 'auth_str') && !isPresent(proxy, 'auth-str') ) { proxy['auth-str'] = proxy['auth_str']; } if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; } } else if (proxy.type === 'wireguard') { proxy.keepalive = proxy.keepalive ?? proxy['persistent-keepalive']; proxy['persistent-keepalive'] = proxy.keepalive; proxy['preshared-key'] = proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['pre-shared-key'] = proxy['preshared-key']; } else if (proxy.type === 'snell' && proxy.version < 3) { delete proxy.udp; } else if (proxy.type === 'vless') { if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } } else if (proxy.type === 'ss') { if ( isPresent(proxy, 'shadow-tls-password') && !isPresent(proxy, 'plugin') ) { proxy.plugin = 'shadow-tls'; proxy['plugin-opts'] = { host: proxy['shadow-tls-sni'], password: proxy['shadow-tls-password'], version: proxy['shadow-tls-version'], }; delete proxy['shadow-tls-password']; delete proxy['shadow-tls-sni']; delete proxy['shadow-tls-version']; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http' ) { let httpPath = proxy['http-opts']?.path; if ( isPresent(proxy, 'http-opts.path') && !Array.isArray(httpPath) ) { proxy['http-opts'].path = [httpPath]; } let httpHost = proxy['http-opts']?.headers?.Host; if ( isPresent(proxy, 'http-opts.headers.Host') && !Array.isArray(httpHost) ) { proxy['http-opts'].headers.Host = [httpHost]; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'h2' ) { let path = proxy['h2-opts']?.path; if ( isPresent(proxy, 'h2-opts.path') && Array.isArray(path) ) { proxy['h2-opts'].path = path[0]; } let host = proxy['h2-opts']?.headers?.host; if ( isPresent(proxy, 'h2-opts.headers.Host') && !Array.isArray(host) ) { proxy['h2-opts'].headers.host = [host]; } } if (['ws'].includes(proxy.network)) { const networkPath = proxy[`${proxy.network}-opts`]?.path; if (networkPath) { const reg = /^(.*?)(?:\?ed=(\d+))?$/; // eslint-disable-next-line no-unused-vars const [_, path = '', ed = ''] = reg.exec(networkPath); proxy[`${proxy.network}-opts`].path = path; if (ed !== '') { proxy['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol'; proxy['ws-opts']['max-early-data'] = parseInt( ed, 10, ); } } else { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].path = '/'; } } if (proxy['plugin-opts']?.tls) { if (isPresent(proxy, 'skip-cert-verify')) { proxy['plugin-opts']['skip-cert-verify'] = proxy['skip-cert-verify']; } } if ( [ 'trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity', 'anytls', 'trusttunnel', 'naive', ].includes(proxy.type) ) { delete proxy.tls; } if (proxy['tls-fingerprint']) { proxy.fingerprint = proxy['tls-fingerprint']; } delete proxy['tls-fingerprint']; if (proxy['underlying-proxy']) { proxy['dialer-proxy'] = proxy['underlying-proxy']; } delete proxy['underlying-proxy']; if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { delete proxy.tls; } delete proxy.subName; delete proxy.collectionName; delete proxy.id; delete proxy.resolved; delete proxy['no-resolve']; if (type !== 'internal' || opts['delete-underscore-fields']) { for (const key in proxy) { if (proxy[key] == null || /^_/i.test(key)) { delete proxy[key]; } } } if ( ['grpc'].includes(proxy.network) && proxy[`${proxy.network}-opts`] ) { delete proxy[`${proxy.network}-opts`]['_grpc-type']; delete proxy[`${proxy.network}-opts`]['_grpc-authority']; } if (proxy['ip-version']) { proxy['ip-version'] = ipVersions[proxy['ip-version']] || proxy['ip-version']; } return proxy; }); return type === 'internal' ? list : 'proxies:\n' + list .map((proxy) => ' - ' + JSON.stringify(proxy) + '\n') .join(''); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/egern.js ================================================ import { isPresent } from './utils'; export default function Egern_Producer() { const type = 'ALL'; const produce = (proxies, type) => { // https://egernapp.com/zh-CN/docs/configuration/proxies const list = proxies .filter((proxy) => { if ( ![ 'http', 'https', 'socks5', 'ss', 'trojan', 'hysteria2', 'vless', 'vmess', 'tuic', 'wireguard', 'anytls', ].includes(proxy.type) || (proxy.type === 'ss' && ((proxy.plugin === 'obfs' && !['http', 'tls'].includes( proxy['plugin-opts']?.mode, )) || ![ 'chacha20-ietf-poly1305', 'chacha20-poly1305', 'aes-256-gcm', 'aes-128-gcm', 'none', 'tbale', 'rc4', 'rc4-md5', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'bf-cfb', 'camellia-128-cfb', 'camellia-192-cfb', 'camellia-256-cfb', 'cast5-cfb', 'des-cfb', 'idea-cfb', 'rc2-cfb', 'seed-cfb', 'salsa20', 'chacha20', 'chacha20-ietf', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', ].includes(proxy.cipher))) || (proxy.type === 'vmess' && !['http', 'ws', 'tcp'].includes(proxy.network) && proxy.network) || (proxy.type === 'trojan' && !['http', 'ws', 'tcp'].includes(proxy.network) && proxy.network) || (proxy.type === 'vless' && ((!['http', 'ws', 'tcp'].includes(proxy.network) && proxy.network) || (typeof proxy.flow !== 'undefined' && !['xtls-rprx-vision', ''].includes( proxy.flow, )))) || (proxy.type === 'tuic' && proxy.token && proxy.token.length !== 0) ) { return false; } else if ( ['anytls'].includes(proxy.type) && proxy.network && (!['tcp'].includes(proxy.network) || (['tcp'].includes(proxy.network) && proxy['reality-opts'])) ) { return false; } else if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { return false; } return true; }) .map((proxy) => { const original = { ...proxy }; let flow; if (proxy.tls && !proxy.sni) { proxy.sni = proxy.server; } const prev_hop = proxy.prev_hop || proxy['underlying-proxy'] || proxy['dialer-proxy'] || proxy.detour; if (proxy.type === 'http') { proxy = { type: proxy.tls ? 'https' : 'http', name: proxy.name, server: proxy.server, port: proxy.port, username: proxy.username, password: proxy.password, tfo: proxy.tfo || proxy['fast-open'], next_hop: proxy.next_hop, ...(proxy.tls ? { sni: proxy.sni, skip_tls_verify: proxy['skip-cert-verify'], } : {}), }; } else if (proxy.type === 'socks5') { proxy = { type: 'socks5', name: proxy.name, server: proxy.server, port: proxy.port, username: proxy.username, password: proxy.password, tfo: proxy.tfo || proxy['fast-open'], udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, }; } else if (proxy.type === 'ss') { proxy = { type: 'shadowsocks', name: proxy.name, method: proxy.cipher === 'chacha20-ietf-poly1305' ? 'chacha20-poly1305' : proxy.cipher, server: proxy.server, port: proxy.port, password: proxy.password, tfo: proxy.tfo || proxy['fast-open'], udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, }; if (original.plugin === 'obfs') { proxy.obfs = original['plugin-opts'].mode; proxy.obfs_host = original['plugin-opts'].host; proxy.obfs_uri = original['plugin-opts'].path; } } else if (proxy.type === 'hysteria2') { proxy = { type: 'hysteria2', name: proxy.name, server: proxy.server, port: proxy.port, auth: proxy.password, tfo: proxy.tfo || proxy['fast-open'], udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, sni: proxy.sni, skip_tls_verify: proxy['skip-cert-verify'], port_hopping: proxy.ports, port_hopping_interval: proxy['hop-interval'], }; if ( original['obfs-password'] && original.obfs == 'salamander' ) { proxy.obfs = 'salamander'; proxy.obfs_password = original['obfs-password']; } } else if (proxy.type === 'tuic') { proxy = { type: 'tuic', name: proxy.name, server: proxy.server, port: proxy.port, uuid: proxy.uuid, password: proxy.password, next_hop: proxy.next_hop, sni: proxy.sni, alpn: Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn || 'h3'], skip_tls_verify: proxy['skip-cert-verify'], port_hopping: proxy.ports, port_hopping_interval: proxy['hop-interval'], }; } else if (proxy.type === 'trojan') { if (proxy.network === 'ws') { proxy.websocket = { path: proxy['ws-opts']?.path, host: proxy['ws-opts']?.headers?.Host, }; } proxy = { type: 'trojan', name: proxy.name, server: proxy.server, port: proxy.port, password: proxy.password, tfo: proxy.tfo || proxy['fast-open'], udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, sni: proxy.sni, skip_tls_verify: proxy['skip-cert-verify'], websocket: proxy.websocket, }; } else if (proxy.type === 'anytls') { proxy = { type: 'anytls', name: proxy.name, server: proxy.server, port: proxy.port, password: proxy.password, tfo: proxy.tfo || proxy['fast-open'], udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, sni: proxy.sni, skip_tls_verify: proxy['skip-cert-verify'], }; } else if (proxy.type === 'vmess') { // Egern:传输层,支持 ws/wss/http1/http2/tls,不配置则为 tcp let security = proxy.cipher; if ( security && ![ 'auto', 'none', 'zero', 'aes-128-gcm', 'chacha20-poly1305', ].includes(security) ) { security = 'auto'; } if (proxy.network === 'ws') { proxy.transport = { [proxy.tls ? 'wss' : 'ws']: { path: proxy['ws-opts']?.path, headers: { Host: proxy['ws-opts']?.headers?.Host, }, sni: proxy.tls ? proxy.sni : undefined, skip_tls_verify: proxy.tls ? proxy['skip-cert-verify'] : undefined, }, }; } else if (proxy.network === 'http') { proxy.transport = { http1: { method: proxy['http-opts']?.method, path: Array.isArray(proxy['http-opts']?.path) ? proxy['http-opts']?.path[0] : proxy['http-opts']?.path, headers: { Host: Array.isArray( proxy['http-opts']?.headers?.Host, ) ? proxy['http-opts']?.headers?.Host[0] : proxy['http-opts']?.headers?.Host, }, skip_tls_verify: proxy['skip-cert-verify'], }, }; } else if (proxy.network === 'h2') { proxy.transport = { http2: { method: proxy['h2-opts']?.method, path: Array.isArray(proxy['h2-opts']?.path) ? proxy['h2-opts']?.path[0] : proxy['h2-opts']?.path, headers: { Host: Array.isArray( proxy['h2-opts']?.headers?.Host, ) ? proxy['h2-opts']?.headers?.Host[0] : proxy['h2-opts']?.headers?.Host, }, skip_tls_verify: proxy['skip-cert-verify'], }, }; } else if ( (proxy.network === 'tcp' || !proxy.network) && proxy.tls ) { proxy.transport = { tls: { sni: proxy.tls ? proxy.sni : undefined, skip_tls_verify: proxy.tls ? proxy['skip-cert-verify'] : undefined, }, }; } let legacy; if (isPresent(proxy, 'aead') && !proxy.aead) { legacy = true; } else if (proxy.alterId !== 0) { legacy = true; } proxy = { type: 'vmess', name: proxy.name, server: proxy.server, port: proxy.port, user_id: proxy.uuid, security, tfo: proxy.tfo || proxy['fast-open'], legacy, udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, transport: proxy.transport, // sni: proxy.sni, // skip_tls_verify: proxy['skip-cert-verify'], }; } else if (proxy.type === 'vless') { if (proxy.encryption && proxy.encryption !== 'none') throw new Error(`VLESS encryption is not supported`); if (proxy.network === 'ws') { proxy.transport = { [proxy.tls ? 'wss' : 'ws']: { path: proxy['ws-opts']?.path, headers: { Host: proxy['ws-opts']?.headers?.Host, }, sni: proxy.tls ? proxy.sni : undefined, skip_tls_verify: proxy.tls ? proxy['skip-cert-verify'] : undefined, }, }; } else if (proxy.network === 'http') { proxy.transport = { http: { method: proxy['http-opts']?.method, path: Array.isArray(proxy['http-opts']?.path) ? proxy['http-opts']?.path[0] : proxy['http-opts']?.path, headers: { Host: Array.isArray( proxy['http-opts']?.headers?.Host, ) ? proxy['http-opts']?.headers?.Host[0] : proxy['http-opts']?.headers?.Host, }, skip_tls_verify: proxy['skip-cert-verify'], }, }; } else if (proxy.network === 'tcp' || !proxy.network) { let reality; if ( proxy['reality-opts']?.['short-id'] || proxy['reality-opts']?.['public-key'] ) { reality = { short_id: proxy['reality-opts']['short-id'], public_key: proxy['reality-opts']['public-key'], }; } proxy.transport = { [proxy.tls ? 'tls' : 'tcp']: { sni: proxy.tls ? proxy.sni : undefined, skip_tls_verify: proxy.tls ? proxy['skip-cert-verify'] : undefined, reality, }, }; flow = proxy.flow; if (flow === '') flow = undefined; } proxy = { type: 'vless', name: proxy.name, server: proxy.server, port: proxy.port, user_id: proxy.uuid, security: proxy.cipher, tfo: proxy.tfo || proxy['fast-open'], udp_relay: proxy.udp || proxy.udp_relay || proxy.udp_relay, next_hop: proxy.next_hop, transport: proxy.transport, flow, // sni: proxy.sni, // skip_tls_verify: proxy['skip-cert-verify'], }; } else if (proxy.type === 'wireguard') { if (Array.isArray(proxy.peers) && proxy.peers.length > 0) { proxy.server = proxy.peers[0].server; proxy.port = proxy.peers[0].port; proxy.ip = proxy.peers[0].ip; proxy.ipv6 = proxy.peers[0].ipv6; proxy['public-key'] = proxy.peers[0]['public-key']; proxy['preshared-key'] = proxy.peers[0]['pre-shared-key']; // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717 proxy['allowed-ips'] = proxy.peers[0]['allowed-ips']; proxy.reserved = proxy.peers[0].reserved; } proxy = { type: 'wireguard', name: proxy.name, local_ipv4: proxy.ip, local_ipv6: proxy.ipv6, server: proxy.server, port: proxy.port, private_key: proxy['private-key'], peer_public_key: proxy['public-key'], preshared_key: proxy['preshared-key'], reserved: proxy.reserved ? Array.isArray(proxy.reserved) ? proxy.reserved : proxy.reserved .split(/\s*\/\s*/) .map((item) => item.trim()) .filter((item) => item.length > 0) : undefined, dns_servers: proxy.dns ? Array.isArray(proxy.dns) ? proxy.dns : proxy.dns .split(/\s*,\s*/) .map((item) => item.trim()) .filter((item) => item.length > 0) : undefined, mtu: proxy.mtu, keepalive: proxy.keepalive, }; } if ( [ 'http', 'https', 'socks5', 'ss', 'trojan', 'vless', 'vmess', 'anytls', ].includes(original.type) ) { if (isPresent(original, 'shadow-tls-password')) { if (original['shadow-tls-version'] != 3) throw new Error( `shadow-tls version ${original['shadow-tls-version']} is not supported`, ); proxy.shadow_tls = { password: original['shadow-tls-password'], sni: original['shadow-tls-sni'], }; } else if ( ['shadow-tls'].includes(original.plugin) && original['plugin-opts'] ) { if (original['plugin-opts'].version != 3) throw new Error( `shadow-tls version ${original['plugin-opts'].version} is not supported`, ); proxy.shadow_tls = { password: original['plugin-opts'].password, sni: original['plugin-opts'].host, }; } } if ( [ 'socks5', 'ss', 'trojan', 'vless', 'vmess', 'wireguard', 'tuic', 'hysteria2', 'anytls', ].includes(original.type) ) { if ( ['on', 'true', true, '1', 1].includes( original['block-quic'], ) ) { proxy.block_quic = true; } else if ( ['off', 'false', false, '0', 0].includes( original['block-quic'], ) ) { proxy.block_quic = false; } } if ( ['ss'].includes(original.type) && proxy.shadow_tls && original['udp-port'] > 0 && original['udp-port'] <= 65535 ) { proxy['udp_port'] = original['udp-port']; } delete proxy.subName; delete proxy.collectionName; delete proxy.id; delete proxy.resolved; delete proxy['no-resolve']; if (proxy.transport) { for (const key in proxy.transport) { if ( Object.keys(proxy.transport[key]).length === 0 || Object.values(proxy.transport[key]).every( (v) => v == null, ) ) { delete proxy.transport[key]; } } if (Object.keys(proxy.transport).length === 0) { delete proxy.transport; } } if (type !== 'internal') { for (const key in proxy) { if (proxy[key] == null || /^_/i.test(key)) { delete proxy[key]; } } } return { [proxy.type]: { ...proxy, type: undefined, prev_hop, }, }; }); return type === 'internal' ? list : 'proxies:\n' + list .map((proxy) => ' - ' + JSON.stringify(proxy) + '\n') .join(''); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/index.js ================================================ import Surge_Producer from './surge'; import SurgeMac_Producer from './surgemac'; import Clash_Producer from './clash'; import ClashMeta_Producer from './clashmeta'; import Stash_Producer from './stash'; import Loon_Producer from './loon'; import URI_Producer from './uri'; import V2Ray_Producer from './v2ray'; import QX_Producer from './qx'; import Shadowrocket_Producer from './shadowrocket'; import Surfboard_Producer from './surfboard'; import singbox_Producer from './sing-box'; import Egern_Producer from './egern'; function JSON_Producer() { const type = 'ALL'; const produce = (proxies, type) => type === 'internal' ? proxies : JSON.stringify(proxies, null, 2); return { type, produce }; } export default { qx: QX_Producer(), QX: QX_Producer(), QuantumultX: QX_Producer(), surge: Surge_Producer(), Surge: Surge_Producer(), SurgeMac: SurgeMac_Producer(), Loon: Loon_Producer(), Clash: Clash_Producer(), meta: ClashMeta_Producer(), clashmeta: ClashMeta_Producer(), 'clash.meta': ClashMeta_Producer(), 'Clash.Meta': ClashMeta_Producer(), ClashMeta: ClashMeta_Producer(), mihomo: ClashMeta_Producer(), Mihomo: ClashMeta_Producer(), uri: URI_Producer(), URI: URI_Producer(), v2: V2Ray_Producer(), v2ray: V2Ray_Producer(), V2Ray: V2Ray_Producer(), json: JSON_Producer(), JSON: JSON_Producer(), stash: Stash_Producer(), Stash: Stash_Producer(), shadowrocket: Shadowrocket_Producer(), Shadowrocket: Shadowrocket_Producer(), ShadowRocket: Shadowrocket_Producer(), surfboard: Surfboard_Producer(), Surfboard: Surfboard_Producer(), singbox: singbox_Producer(), 'sing-box': singbox_Producer(), egern: Egern_Producer(), Egern: Egern_Producer(), }; ================================================ FILE: backend/src/core/proxy-utils/producers/loon.js ================================================ /* eslint-disable no-case-declarations */ const targetPlatform = 'Loon'; import { isPresent, Result } from './utils'; import { isIPv4, isIPv6 } from '@/utils'; import $ from '@/core/app'; const ipVersions = { dual: 'dual', ipv4: 'v4-only', ipv6: 'v6-only', 'ipv4-prefer': 'prefer-v4', 'ipv6-prefer': 'prefer-v6', }; export default function Loon_Producer() { const produce = (proxy, type, opts = {}) => { if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { throw new Error( `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`, ); } switch (proxy.type) { case 'ss': return shadowsocks(proxy); case 'ssr': return shadowsocksr(proxy); case 'trojan': return trojan(proxy); case 'anytls': return anytls(proxy); case 'vmess': return vmess(proxy, opts['include-unsupported-proxy']); case 'vless': return vless(proxy, opts['include-unsupported-proxy']); case 'http': return http(proxy); case 'socks5': return socks5(proxy); case 'wireguard': return wireguard(proxy); case 'hysteria2': return hysteria2(proxy); } throw new Error( `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, ); }; return { produce }; } function shadowsocks(proxy) { const result = new Result(proxy); if ( ![ 'rc4', 'rc4-md5', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'bf-cfb', 'camellia-128-cfb', 'camellia-192-cfb', 'camellia-256-cfb', 'salsa20', 'chacha20', 'chacha20-ietf', 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', ].includes(proxy.cipher) ) { throw new Error(`cipher ${proxy.cipher} is not supported`); } result.append( `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`, ); // obfs if (isPresent(proxy, 'plugin')) { if (proxy.plugin === 'obfs') { if ( proxy['plugin-opts']?.mode && proxy.cipher.startsWith('2022-') ) { throw new Error( `${proxy.cipher} ${proxy.plugin} is not supported`, ); } result.append(`,obfs-name=${proxy['plugin-opts'].mode}`); result.appendIfPresent( `,obfs-host=${proxy['plugin-opts'].host}`, 'plugin-opts.host', ); result.appendIfPresent( `,obfs-uri=${proxy['plugin-opts'].path}`, 'plugin-opts.path', ); } else if (!['shadow-tls'].includes(proxy.plugin)) { throw new Error(`plugin ${proxy.plugin} is not supported`); } } // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); // udp-port result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port'); } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) { const password = proxy['plugin-opts'].password; const host = proxy['plugin-opts'].host; const version = proxy['plugin-opts'].version; if (password) { result.append(`,shadow-tls-password=${password}`); if (host) { result.append(`,shadow-tls-sni=${host}`); } if (version) { if (version < 2) { throw new Error( `shadow-tls version ${version} is not supported`, ); } result.append(`,shadow-tls-version=${version}`); } // udp-port result.appendIfPresent( `,udp-port=${proxy['udp-port']}`, 'udp-port', ); } } // udp over tcp if (proxy['udp-over-tcp']) { if (proxy['udp-over-tcp-version'] === 2) { if (proxy.plugin === 'obfs') { $.error( `Platform ${targetPlatform} shadowsocks udp-over-tcp does not support obfs`, ); } else { result.append(`,udp-over-tcp=true`); } } else { $.error( `Platform ${targetPlatform} shadowsocks only supports udp-over-tcp-version 2`, ); } } // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function shadowsocksr(proxy) { const result = new Result(proxy); result.append( `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`, ); // ssr protocol result.append(`,protocol=${proxy.protocol}`); result.appendIfPresent( `,protocol-param=${proxy['protocol-param']}`, 'protocol-param', ); // obfs result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs'); result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); // udp-port result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port'); } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) { const password = proxy['plugin-opts'].password; const host = proxy['plugin-opts'].host; const version = proxy['plugin-opts'].version; if (password) { result.append(`,shadow-tls-password=${password}`); if (host) { result.append(`,shadow-tls-sni=${host}`); } if (version) { if (version < 2) { throw new Error( `shadow-tls version ${version} is not supported`, ); } result.append(`,shadow-tls-version=${version}`); } // udp-port result.appendIfPresent( `,udp-port=${proxy['udp-port']}`, 'udp-port', ); } } // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function trojan(proxy) { const result = new Result(proxy); result.append( `${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`, ); if (proxy.network === 'tcp') { delete proxy.network; } // transport if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { result.append(`,transport=ws`); result.appendIfPresent( `,path=${proxy['ws-opts']?.path}`, 'ws-opts.path', ); result.appendIfPresent( `,host=${proxy['ws-opts']?.headers?.Host}`, 'ws-opts.headers.Host', ); } else { throw new Error(`network ${proxy.network} is unsupported`); } } // tls verification result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // sni result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); result.appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function anytls(proxy) { const result = new Result(proxy); result.append( `${proxy.name}=anytls,${proxy.server},${proxy.port},"${proxy.password}"`, ); // 新版删除idle-session-check-interval和min-idle-session 参数,session 改为主动超时机制,由于 anytls-go 不支持一个tcp 并发多个 stream,max-stream-cout 设置大于 1 时会有阻塞,如果有其他支持多路复用的 anytls 服务器实现,可以设置max-stream-cout 大于 1 for (const key of [ // 'idle-session-check-interval', 'idle-session-timeout', // 'min-idle-session', 'max-stream-count', ]) { // 值为整数 才附加 if (isPresent(proxy, key) && Number.isInteger(proxy[key])) { result.append(`,${key}=${proxy[key]}`); } } // tls verification result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // sni result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); result.appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function vmess(proxy) { const isReality = !!proxy['reality-opts']; const result = new Result(proxy); result.append( `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`, ); if (proxy.network === 'tcp') { delete proxy.network; } // transport if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { result.append(`,transport=ws`); result.appendIfPresent( `,path=${proxy['ws-opts']?.path}`, 'ws-opts.path', ); result.appendIfPresent( `,host=${proxy['ws-opts']?.headers?.Host}`, 'ws-opts.headers.Host', ); } else if (proxy.network === 'http') { result.append(`,transport=http`); let httpPath = proxy['http-opts']?.path; let httpHost = proxy['http-opts']?.headers?.Host; result.appendIfPresent( `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`, 'http-opts.path', ); result.appendIfPresent( `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`, 'http-opts.headers.Host', ); } else { throw new Error(`network ${proxy.network} is unsupported`); } } else { result.append(`,transport=tcp`); } // tls result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); // tls verification result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); if (isReality) { result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); result.appendIfPresent( `,public-key="${proxy['reality-opts']['public-key']}"`, 'reality-opts.public-key', ); result.appendIfPresent( `,short-id=${proxy['reality-opts']['short-id']}`, 'reality-opts.short-id', ); } else { // sni result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); result.appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); } // AEAD if (isPresent(proxy, 'aead')) { result.append(`,alterId=${proxy.aead ? 0 : 1}`); } else { result.append(`,alterId=${proxy.alterId}`); } // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function vless(proxy) { if (proxy.encryption && proxy.encryption !== 'none') throw new Error(`VLESS encryption is not supported`); let isXtls = false; const isReality = !!proxy['reality-opts']; if (typeof proxy.flow !== 'undefined') { if (['xtls-rprx-vision'].includes(proxy.flow)) { isXtls = true; } else { throw new Error(`VLESS flow(${proxy.flow}) is not supported`); } } const result = new Result(proxy); result.append( `${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`, ); if (proxy.network === 'tcp') { delete proxy.network; } // transport if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { result.append(`,transport=ws`); result.appendIfPresent( `,path=${proxy['ws-opts']?.path}`, 'ws-opts.path', ); result.appendIfPresent( `,host=${proxy['ws-opts']?.headers?.Host}`, 'ws-opts.headers.Host', ); } else if (proxy.network === 'http') { result.append(`,transport=http`); let httpPath = proxy['http-opts']?.path; let httpHost = proxy['http-opts']?.headers?.Host; result.appendIfPresent( `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`, 'http-opts.path', ); result.appendIfPresent( `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`, 'http-opts.headers.Host', ); } else { throw new Error(`network ${proxy.network} is unsupported`); } } else { result.append(`,transport=tcp`); } // tls result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); // tls verification result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); if (isXtls) { result.appendIfPresent(`,flow=${proxy.flow}`, 'flow'); } if (isReality) { result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); result.appendIfPresent( `,public-key="${proxy['reality-opts']['public-key']}"`, 'reality-opts.public-key', ); result.appendIfPresent( `,short-id=${proxy['reality-opts']['short-id']}`, 'reality-opts.short-id', ); } else { // sni result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); result.appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); } // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function http(proxy) { const result = new Result(proxy); const type = proxy.tls ? 'https' : 'http'; result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,"${proxy.password}"`, 'password'); // sni result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); // tls verification result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function socks5(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=socks5,${proxy.server},${proxy.port}`); result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,"${proxy.password}"`, 'password'); // tls result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); // sni result.appendIfPresent(`,sni=${proxy.sni}`, 'sni'); // tls verification result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } function wireguard(proxy) { if (Array.isArray(proxy.peers) && proxy.peers.length > 0) { proxy.server = proxy.peers[0].server; proxy.port = proxy.peers[0].port; proxy.ip = proxy.peers[0].ip; proxy.ipv6 = proxy.peers[0].ipv6; proxy['public-key'] = proxy.peers[0]['public-key']; proxy['preshared-key'] = proxy.peers[0]['pre-shared-key']; // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717 proxy['allowed-ips'] = proxy.peers[0]['allowed-ips']; proxy.reserved = proxy.peers[0].reserved; } const result = new Result(proxy); result.append(`${proxy.name}=wireguard`); result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip'); result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6'); result.appendIfPresent( `,private-key="${proxy['private-key']}"`, 'private-key', ); result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu'); if (proxy.dns) { if (Array.isArray(proxy.dns)) { proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i)); let dns = proxy.dns.find((i) => isIPv4(i)); if (!dns) { dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i)); } proxy.dns = dns; } } result.appendIfPresent(`,dns=${proxy.dns}`, 'dns'); result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6'); result.appendIfPresent( `,keepalive=${proxy['persistent-keepalive']}`, 'persistent-keepalive', ); result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive'); const allowedIps = Array.isArray(proxy['allowed-ips']) ? proxy['allowed-ips'].join(',') : proxy['allowed-ips']; let reserved = Array.isArray(proxy.reserved) ? proxy.reserved.join(',') : proxy.reserved; if (reserved) { reserved = `,reserved=[${reserved}]`; } let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key']; if (presharedKey) { presharedKey = `,preshared-key="${presharedKey}"`; } result.append( `,peers=[{public-key="${proxy['public-key']}",allowed-ips="${ allowedIps ?? '0.0.0.0/0,::/0' }",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${ presharedKey ?? '' }}]`, ); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } return result.toString(); } function hysteria2(proxy) { if (proxy['obfs-password'] && proxy.obfs != 'salamander') { throw new Error(`only salamander obfs is supported`); } const result = new Result(proxy); result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`); result.appendIfPresent(`,"${proxy.password}"`, 'password'); // sni result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); result.appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); if (proxy['obfs-password'] && proxy.obfs == 'salamander') { result.append(`,salamander-password=${proxy['obfs-password']}`); } // tfo result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // block-quic if (proxy['block-quic'] === 'on') { result.append(',block-quic=true'); } else if (proxy['block-quic'] === 'off') { result.append(',block-quic=false'); } // udp if (proxy.udp) { result.append(`,udp=true`); } // download-bandwidth result.appendIfPresent( `,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`, 'down', ); result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version'); return result.toString(); } ================================================ FILE: backend/src/core/proxy-utils/producers/qx.js ================================================ import { isPresent, Result } from './utils'; const targetPlatform = 'QX'; export default function QX_Producer() { // eslint-disable-next-line no-unused-vars const produce = (proxy, type, opts = {}) => { if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { throw new Error( `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`, ); } switch (proxy.type) { case 'ss': return shadowsocks(proxy); case 'ssr': return shadowsocksr(proxy); case 'trojan': return trojan(proxy); case 'vmess': return vmess(proxy); case 'http': return http(proxy); case 'socks5': return socks5(proxy); case 'vless': return vless(proxy); } throw new Error( `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, ); }; return { produce: (proxy, type, opts = {}) => { let result = produce(proxy, type, opts); if (proxy.flow && proxy.flow !== 'xtls-rprx-vision') { throw new Error( `Platform ${targetPlatform} does not support flow ${proxy.flow}`, ); } if (proxy['reality-opts']) { if (proxy['reality-opts']['public-key']) { result = `${result},reality-base64-pubkey=${proxy['reality-opts']['public-key']}`; } if (proxy['reality-opts']['short-id']) { result = `${result},reality-hex-shortid=${proxy['reality-opts']['short-id']}`; } } return result; }, }; } function shadowsocks(proxy) { const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); if (!proxy.cipher) { proxy.cipher = 'none'; } if ( ![ 'none', 'rc4-md5', 'rc4-md5-6', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'bf-cfb', 'cast5-cfb', 'des-cfb', 'rc2-cfb', 'salsa20', 'chacha20', 'chacha20-ietf', 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', ].includes(proxy.cipher) ) { throw new Error(`cipher ${proxy.cipher} is not supported`); } append(`shadowsocks=${proxy.server}:${proxy.port}`); append(`,method=${proxy.cipher}`); append(`,password=${proxy.password}`); // obfs if (needTls(proxy)) { proxy.tls = true; } if (isPresent(proxy, 'plugin')) { if (proxy.plugin === 'obfs') { const opts = proxy['plugin-opts']; append(`,obfs=${opts.mode}`); } else if ( proxy.plugin === 'v2ray-plugin' && proxy['plugin-opts'].mode === 'websocket' ) { const opts = proxy['plugin-opts']; if (opts.tls) append(`,obfs=wss`); else append(`,obfs=ws`); } else { throw new Error(`plugin is not supported`); } appendIfPresent( `,obfs-host=${proxy['plugin-opts'].host}`, 'plugin-opts.host', ); appendIfPresent( `,obfs-uri=${proxy['plugin-opts'].path}`, 'plugin-opts.path', ); } if (needTls(proxy)) { appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn'); appendIfPresent( `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`, 'tls-no-session-ticket', ); appendIfPresent( `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`, 'tls-no-session-reuse', ); // tls fingerprint appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification appendIfPresent( `,tls-verification=${!proxy['skip-cert-verify']}`, 'skip-cert-verify', ); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); } // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // udp over tcp if (proxy['_ssr_python_uot']) { append(`,udp-over-tcp=true`); } else if (proxy['udp-over-tcp']) { if ( !proxy['udp-over-tcp-version'] || proxy['udp-over-tcp-version'] === 1 ) { append(`,udp-over-tcp=sp.v1`); } else if (proxy['udp-over-tcp-version'] === 2) { append(`,udp-over-tcp=sp.v2`); } } // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function shadowsocksr(proxy) { const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); append(`shadowsocks=${proxy.server}:${proxy.port}`); append(`,method=${proxy.cipher}`); append(`,password=${proxy.password}`); // ssr protocol append(`,ssr-protocol=${proxy.protocol}`); appendIfPresent( `,ssr-protocol-param=${proxy['protocol-param']}`, 'protocol-param', ); // obfs appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs'); appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param'); // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function trojan(proxy) { const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); append(`trojan=${proxy.server}:${proxy.port}`); append(`,password=${proxy.password}`); // obfs ws if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { if (needTls(proxy)) append(`,obfs=wss`); else append(`,obfs=ws`); appendIfPresent( `,obfs-uri=${proxy['ws-opts']?.path}`, 'ws-opts.path', ); appendIfPresent( `,obfs-host=${proxy['ws-opts']?.headers?.Host}`, 'ws-opts.headers.Host', ); } else if (!['tcp'].includes(proxy.network)) { throw new Error(`network ${proxy.network} is unsupported`); } } // over tls if (proxy.network !== 'ws' && needTls(proxy)) { append(`,over-tls=true`); } if (needTls(proxy)) { appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn'); appendIfPresent( `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`, 'tls-no-session-ticket', ); appendIfPresent( `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`, 'tls-no-session-reuse', ); // tls fingerprint appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification appendIfPresent( `,tls-verification=${!proxy['skip-cert-verify']}`, 'skip-cert-verify', ); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); } // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function vmess(proxy) { const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); append(`vmess=${proxy.server}:${proxy.port}`); // cipher let cipher; if (proxy.cipher === 'auto') { cipher = 'chacha20-ietf-poly1305'; } else { cipher = proxy.cipher; } append(`,method=${cipher}`); append(`,password=${proxy.uuid}`); // obfs if (needTls(proxy)) { proxy.tls = true; } if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { if (proxy.tls) append(`,obfs=wss`); else append(`,obfs=ws`); } else if (proxy.network === 'http') { append(`,obfs=http`); } else if (['tcp'].includes(proxy.network)) { if (proxy.tls) append(`,obfs=over-tls`); } else if (!['tcp'].includes(proxy.network)) { throw new Error(`network ${proxy.network} is unsupported`); } let transportPath = proxy[`${proxy.network}-opts`]?.path; let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; appendIfPresent( `,obfs-uri=${ Array.isArray(transportPath) ? transportPath[0] : transportPath }`, `${proxy.network}-opts.path`, ); appendIfPresent( `,obfs-host=${ Array.isArray(transportHost) ? transportHost[0] : transportHost }`, `${proxy.network}-opts.headers.Host`, ); } else { // over-tls if (proxy.tls) append(`,obfs=over-tls`); } if (needTls(proxy)) { appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn'); appendIfPresent( `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`, 'tls-no-session-ticket', ); appendIfPresent( `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`, 'tls-no-session-reuse', ); // tls fingerprint appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification appendIfPresent( `,tls-verification=${!proxy['skip-cert-verify']}`, 'skip-cert-verify', ); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); } // AEAD if (isPresent(proxy, 'aead')) { append(`,aead=${proxy.aead}`); } else { append(`,aead=${proxy.alterId === 0}`); } // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function vless(proxy) { if (proxy.encryption && proxy.encryption !== 'none') throw new Error(`VLESS encryption is not supported`); const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); append(`vless=${proxy.server}:${proxy.port}`); // The method field for vless should be none. let cipher = 'none'; // if (proxy.cipher === 'auto') { // cipher = 'chacha20-ietf-poly1305'; // } else { // cipher = proxy.cipher; // } append(`,method=${cipher}`); append(`,password=${proxy.uuid}`); // obfs if (needTls(proxy)) { proxy.tls = true; } if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { if (proxy.tls) append(`,obfs=wss`); else append(`,obfs=ws`); } else if (proxy.network === 'http') { append(`,obfs=http`); } else if (['tcp'].includes(proxy.network)) { if (proxy.tls) append(`,obfs=over-tls`); } else if (!['tcp'].includes(proxy.network)) { throw new Error(`network ${proxy.network} is unsupported`); } let transportPath = proxy[`${proxy.network}-opts`]?.path; let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; appendIfPresent( `,obfs-uri=${ Array.isArray(transportPath) ? transportPath[0] : transportPath }`, `${proxy.network}-opts.path`, ); appendIfPresent( `,obfs-host=${ Array.isArray(transportHost) ? transportHost[0] : transportHost }`, `${proxy.network}-opts.headers.Host`, ); } else { // over-tls if (proxy.tls) append(`,obfs=over-tls`); } if (needTls(proxy)) { appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn'); appendIfPresent( `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`, 'tls-no-session-ticket', ); appendIfPresent( `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`, 'tls-no-session-reuse', ); // tls fingerprint appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification appendIfPresent( `,tls-verification=${!proxy['skip-cert-verify']}`, 'skip-cert-verify', ); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); } appendIfPresent(`,vless-flow=${proxy.flow}`, 'flow'); // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function http(proxy) { const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); append(`http=${proxy.server}:${proxy.port}`); appendIfPresent(`,username=${proxy.username}`, 'username'); appendIfPresent(`,password=${proxy.password}`, 'password'); // tls if (needTls(proxy)) { proxy.tls = true; } appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); if (needTls(proxy)) { appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn'); appendIfPresent( `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`, 'tls-no-session-ticket', ); appendIfPresent( `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`, 'tls-no-session-reuse', ); // tls fingerprint appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification appendIfPresent( `,tls-verification=${!proxy['skip-cert-verify']}`, 'skip-cert-verify', ); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); } // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function socks5(proxy) { const result = new Result(proxy); const append = result.append.bind(result); const appendIfPresent = result.appendIfPresent.bind(result); append(`socks5=${proxy.server}:${proxy.port}`); appendIfPresent(`,username=${proxy.username}`, 'username'); appendIfPresent(`,password=${proxy.password}`, 'password'); // tls if (needTls(proxy)) { proxy.tls = true; } appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); if (needTls(proxy)) { appendIfPresent( `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, 'tls-pubkey-sha256', ); appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn'); appendIfPresent( `,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`, 'tls-no-session-ticket', ); appendIfPresent( `,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`, 'tls-no-session-reuse', ); // tls fingerprint appendIfPresent( `,tls-cert-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification appendIfPresent( `,tls-verification=${!proxy['skip-cert-verify']}`, 'skip-cert-verify', ); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); } // tfo appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); // udp appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // server_check_url result.appendIfPresent( `,server_check_url=${proxy['test-url']}`, 'test-url', ); // tag append(`,tag=${proxy.name}`); return result.toString(); } function needTls(proxy) { return proxy.tls; } ================================================ FILE: backend/src/core/proxy-utils/producers/shadowrocket.js ================================================ import { isPresent } from '@/core/proxy-utils/producers/utils'; import $ from '@/core/app'; export default function Shadowrocket_Producer() { const type = 'ALL'; const produce = (proxies, type, opts = {}) => { const list = proxies .filter((proxy) => { if (opts['include-unsupported-proxy']) return true; if (proxy.type === 'snell' && proxy.version >= 4) { return false; } else if ( [ 'trusttunnel', 'mieru', 'sudoku', 'naive', 'masque', ].includes(proxy.type) ) { return false; } else if ( proxy.encryption && proxy.encryption !== 'none' && ['vless'].includes(proxy.type) ) { return false; } else if ( ['anytls'].includes(proxy.type) && proxy.network && (!['tcp'].includes(proxy.network) || (['tcp'].includes(proxy.network) && proxy['reality-opts'])) ) { return false; } else if (['xhttp'].includes(proxy.network)) { return false; } return true; }) .map((proxy) => { if (proxy.type === 'vmess') { // handle vmess aead if (isPresent(proxy, 'aead')) { if (proxy.aead) { proxy.alterId = 0; } delete proxy.aead; } if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400 // https://stash.wiki/proxy-protocols/proxy-types#vmess if ( isPresent(proxy, 'cipher') && ![ 'auto', 'none', 'zero', 'aes-128-gcm', 'chacha20-poly1305', ].includes(proxy.cipher) ) { proxy.cipher = 'auto'; } } else if (proxy.type === 'tuic') { if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } // else { // proxy.alpn = ['h3']; // } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; } // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 if ( (!proxy.token || proxy.token.length === 0) && !isPresent(proxy, 'version') ) { proxy.version = 5; } } else if (proxy.type === 'hysteria') { // auth_str 将会在未来某个时候删除 但是有的机场不规范 if ( isPresent(proxy, 'auth_str') && !isPresent(proxy, 'auth-str') ) { proxy['auth-str'] = proxy['auth_str']; } if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; } } else if (proxy.type === 'hysteria2') { // 新版已更改 // if (proxy['obfs-password'] && proxy.obfs == 'salamander') { // proxy.obfs = proxy['obfs-password']; // delete proxy['obfs-password']; // } if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; } } else if (proxy.type === 'wireguard') { proxy.keepalive = proxy.keepalive ?? proxy['persistent-keepalive']; proxy['persistent-keepalive'] = proxy.keepalive; proxy['preshared-key'] = proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['pre-shared-key'] = proxy['preshared-key']; } else if (proxy.type === 'snell' && proxy.version < 3) { delete proxy.udp; } else if (proxy.type === 'vless') { if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } } else if (proxy.type === 'ss') { if ( isPresent(proxy, 'shadow-tls-password') && !isPresent(proxy, 'plugin') ) { proxy.plugin = 'shadow-tls'; proxy['plugin-opts'] = { host: proxy['shadow-tls-sni'], password: proxy['shadow-tls-password'], version: proxy['shadow-tls-version'], }; delete proxy['shadow-tls-password']; delete proxy['shadow-tls-sni']; delete proxy['shadow-tls-version']; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http' ) { let httpPath = proxy['http-opts']?.path; if ( isPresent(proxy, 'http-opts.path') && !Array.isArray(httpPath) ) { proxy['http-opts'].path = [httpPath]; } let httpHost = proxy['http-opts']?.headers?.Host; if ( isPresent(proxy, 'http-opts.headers.Host') && !Array.isArray(httpHost) ) { proxy['http-opts'].headers.Host = [httpHost]; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'h2' ) { let path = proxy['h2-opts']?.path; if ( isPresent(proxy, 'h2-opts.path') && Array.isArray(path) ) { proxy['h2-opts'].path = path[0]; } let host = proxy['h2-opts']?.headers?.host; if ( isPresent(proxy, 'h2-opts.headers.Host') && !Array.isArray(host) ) { proxy['h2-opts'].headers.host = [host]; } } if (['ws'].includes(proxy.network)) { const networkPath = proxy[`${proxy.network}-opts`]?.path; if (networkPath) { const reg = /^(.*?)(?:\?ed=(\d+))?$/; // eslint-disable-next-line no-unused-vars const [_, path = '', ed = ''] = reg.exec(networkPath); proxy[`${proxy.network}-opts`].path = path; if (ed !== '') { proxy['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol'; proxy['ws-opts']['max-early-data'] = parseInt( ed, 10, ); } } else { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].path = '/'; } } if (proxy['plugin-opts']?.tls) { if (isPresent(proxy, 'skip-cert-verify')) { proxy['plugin-opts']['skip-cert-verify'] = proxy['skip-cert-verify']; } } if ( [ 'trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity', 'anytls', 'trusttunnel', 'naive', ].includes(proxy.type) ) { delete proxy.tls; } if (proxy['tls-fingerprint']) { proxy.fingerprint = proxy['tls-fingerprint']; } delete proxy['tls-fingerprint']; if (proxy['underlying-proxy']) { proxy['dialer-proxy'] = proxy['underlying-proxy']; } delete proxy['underlying-proxy']; if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { delete proxy.tls; } delete proxy.subName; delete proxy.collectionName; delete proxy.id; delete proxy.resolved; delete proxy['no-resolve']; if (type !== 'internal') { for (const key in proxy) { if (proxy[key] == null || /^_/i.test(key)) { delete proxy[key]; } } } if ( ['grpc'].includes(proxy.network) && proxy[`${proxy.network}-opts`] ) { delete proxy[`${proxy.network}-opts`]['_grpc-type']; delete proxy[`${proxy.network}-opts`]['_grpc-authority']; } return proxy; }); return type === 'internal' ? list : 'proxies:\n' + list .map((proxy) => { return ' - ' + JSON.stringify(proxy) + '\n'; }) .join(''); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/sing-box.js ================================================ import ClashMeta_Producer from './clashmeta'; import $ from '@/core/app'; import { isIPv4, isIPv6, isPlainObject } from '@/utils'; const ipVersions = { ipv4: 'ipv4_only', ipv6: 'ipv6_only', 'v4-only': 'ipv4_only', 'v6-only': 'ipv6_only', 'ipv4-prefer': 'prefer_ipv4', 'ipv6-prefer': 'prefer_ipv6', 'prefer-v4': 'prefer_ipv4', 'prefer-v6': 'prefer_ipv6', }; const ipVersionParser = (proxy, parsedProxy) => { const strategy = ipVersions[proxy['ip-version']]; if (proxy._dns_server && strategy) { parsedProxy.domain_resolver = { server: proxy._dns_server, strategy, }; } }; const detourParser = (proxy, parsedProxy) => { parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour; }; const networkParser = (proxy, parsedProxy) => { if (['tcp', 'udp'].includes(proxy._network)) parsedProxy.network = proxy._network; }; const tfoParser = (proxy, parsedProxy) => { parsedProxy.tcp_fast_open = false; if (proxy.tfo) parsedProxy.tcp_fast_open = true; if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true; if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true; if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open; }; const smuxParser = (smux, proxy) => { if (!smux || !smux.enabled) return; proxy.multiplex = { enabled: true }; proxy.multiplex.protocol = smux.protocol; if (smux['max-connections']) proxy.multiplex.max_connections = parseInt( `${smux['max-connections']}`, 10, ); if (smux['max-streams']) proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10); if (smux['min-streams']) proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10); if (smux.padding) proxy.multiplex.padding = true; if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) { proxy.multiplex.brutal = { enabled: true, }; if (smux['brutal-opts']?.up) proxy.multiplex.brutal.up_mbps = parseInt( `${smux['brutal-opts']?.up}`, 10, ); if (smux['brutal-opts']?.down) proxy.multiplex.brutal.down_mbps = parseInt( `${smux['brutal-opts']?.down}`, 10, ); } }; const wsParser = (proxy, parsedProxy) => { const transport = { type: 'ws', headers: {} }; if (proxy['ws-opts']) { const { path: wsPath = '', headers: wsHeaders = {}, 'max-early-data': max_early_data, 'early-data-header-name': early_data_header_name, } = proxy['ws-opts']; transport.early_data_header_name = early_data_header_name; transport.max_early_data = max_early_data ? parseInt(max_early_data, 10) : undefined; if (wsPath !== '') transport.path = `${wsPath}`; if (Object.keys(wsHeaders).length > 0) { const headers = {}; for (const key of Object.keys(wsHeaders)) { let value = wsHeaders[key]; if (value === '') continue; if (!Array.isArray(value)) value = [`${value}`]; if (value.length > 0) headers[key] = value; } const { Host: wsHost } = headers; if (wsHost.length === 1) for (const item of `Host:${wsHost[0]}`.split('\n')) { const [key, value] = item.split(':'); if (value.trim() === '') continue; headers[key.trim()] = value.trim().split(','); } transport.headers = headers; } } if (proxy['ws-headers']) { const headers = {}; for (const key of Object.keys(proxy['ws-headers'])) { let value = proxy['ws-headers'][key]; if (value === '') continue; if (!Array.isArray(value)) value = [`${value}`]; if (value.length > 0) headers[key] = value; } const { Host: wsHost } = headers; if (wsHost.length === 1) for (const item of `Host:${wsHost[0]}`.split('\n')) { const [key, value] = item.split(':'); if (value.trim() === '') continue; headers[key.trim()] = value.trim().split(','); } for (const key of Object.keys(headers)) transport.headers[key] = headers[key]; } if (proxy['ws-path'] && proxy['ws-path'] !== '') transport.path = `${proxy['ws-path']}`; if (transport.path) { const reg = /^(.*?)(?:\?ed=(\d+))?$/; // eslint-disable-next-line no-unused-vars const [_, path = '', ed = ''] = reg.exec(transport.path); transport.path = path; if (ed !== '') { transport.early_data_header_name = 'Sec-WebSocket-Protocol'; transport.max_early_data = parseInt(ed, 10); } } if (parsedProxy.tls.insecure) parsedProxy.tls.server_name = transport.headers.Host[0]; if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) { transport.type = 'httpupgrade'; if (transport.headers.Host) { transport.host = transport.headers.Host[0]; delete transport.headers.Host; } if (transport.max_early_data) delete transport.max_early_data; if (transport.early_data_header_name) delete transport.early_data_header_name; } for (const key of Object.keys(transport.headers)) { const value = transport.headers[key]; if (value.length === 1) transport.headers[key] = value[0]; } parsedProxy.transport = transport; }; const h1Parser = (proxy, parsedProxy) => { const transport = { type: 'http', headers: {} }; if (proxy['http-opts']) { const { method = '', path: h1Path = '', headers: h1Headers = {}, } = proxy['http-opts']; if (method !== '') transport.method = method; if (Array.isArray(h1Path)) { transport.path = `${h1Path[0]}`; } else if (h1Path !== '') transport.path = `${h1Path}`; for (const key of Object.keys(h1Headers)) { let value = h1Headers[key]; if (value === '') continue; if (key.toLowerCase() === 'host') { let host = value; if (!Array.isArray(host)) host = `${host}`.split(',').map((i) => i.trim()); if (host.length > 0) transport.host = host; continue; } if (!Array.isArray(value)) value = `${value}`.split(',').map((i) => i.trim()); if (value.length > 0) transport.headers[key] = value; } } if (proxy['http-host'] && proxy['http-host'] !== '') { let host = proxy['http-host']; if (!Array.isArray(host)) host = `${host}`.split(',').map((i) => i.trim()); if (host.length > 0) transport.host = host; } // if (!transport.host) return; if (proxy['http-path'] && proxy['http-path'] !== '') { const path = proxy['http-path']; if (Array.isArray(path)) { transport.path = `${path[0]}`; } else if (path !== '') transport.path = `${path}`; } if (parsedProxy.tls.insecure) parsedProxy.tls.server_name = transport.host[0]; if (transport.host?.length === 1) transport.host = transport.host[0]; for (const key of Object.keys(transport.headers)) { const value = transport.headers[key]; if (value.length === 1) transport.headers[key] = value[0]; } parsedProxy.transport = transport; }; const h2Parser = (proxy, parsedProxy) => { const transport = { type: 'http' }; if (proxy['h2-opts']) { let { host = '', path = '' } = proxy['h2-opts']; if (path !== '') transport.path = `${path}`; if (host !== '') { if (!Array.isArray(host)) host = `${host}`.split(',').map((i) => i.trim()); if (host.length > 0) transport.host = host; } } if (proxy['h2-host'] && proxy['h2-host'] !== '') { let host = proxy['h2-host']; if (!Array.isArray(host)) host = `${host}`.split(',').map((i) => i.trim()); if (host.length > 0) transport.host = host; } if (proxy['h2-path'] && proxy['h2-path'] !== '') transport.path = `${proxy['h2-path']}`; parsedProxy.tls.enabled = true; if (parsedProxy.tls.insecure) parsedProxy.tls.server_name = transport.host[0]; if (transport.host.length === 1) transport.host = transport.host[0]; parsedProxy.transport = transport; }; const grpcParser = (proxy, parsedProxy) => { const transport = { type: 'grpc' }; if (proxy['grpc-opts']) { const serviceName = proxy['grpc-opts']['grpc-service-name']; if (serviceName != null && serviceName !== '') transport.service_name = `${serviceName}`; } parsedProxy.transport = transport; }; const tlsParser = (proxy, parsedProxy) => { if (proxy.tls) parsedProxy.tls.enabled = true; if (proxy.servername && proxy.servername !== '') parsedProxy.tls.server_name = proxy.servername; if (proxy.peer && proxy.peer !== '') parsedProxy.tls.server_name = proxy.peer; if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni; if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true; if (proxy.insecure) parsedProxy.tls.insecure = true; if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true; if (typeof proxy.alpn === 'string') { parsedProxy.tls.alpn = [proxy.alpn]; } else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn; if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`; if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str]; if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']]; if (proxy['reality-opts']) { parsedProxy.tls.reality = { enabled: true }; if (proxy['reality-opts']['public-key']) parsedProxy.tls.reality.public_key = proxy['reality-opts']['public-key']; if (proxy['reality-opts']['short-id']) parsedProxy.tls.reality.short_id = proxy['reality-opts']['short-id']; parsedProxy.tls.utls = { enabled: true }; } if ( !['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) && proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '' ) parsedProxy.tls.utls = { enabled: true, fingerprint: proxy['client-fingerprint'], }; if (proxy._ech && isPlainObject(proxy._ech)) { parsedProxy.tls.ech = proxy._ech; } if (proxy._curve_preferences && Array.isArray(proxy._curve_preferences)) { parsedProxy.tls.curve_preferences = proxy._curve_preferences; } if (proxy['_fragment']) parsedProxy.tls.fragment = !!proxy['_fragment']; if (proxy['_fragment_fallback_delay']) parsedProxy.tls.fragment_fallback_delay = proxy['_fragment_fallback_delay']; if (proxy['_record_fragment']) parsedProxy.tls.record_fragment = !!proxy['_record_fragment']; if (proxy['_certificate']) parsedProxy.tls.certificate = proxy['_certificate']; if (proxy['_certificate_path']) parsedProxy.tls.certificate_path = proxy['_certificate_path']; if (proxy['_certificate_public_key_sha256']) parsedProxy.tls.certificate_public_key_sha256 = proxy['_certificate_public_key_sha256']; if (proxy['_client_certificate']) parsedProxy.tls.client_certificate = proxy['_client_certificate']; if (proxy['_client_certificate_path']) parsedProxy.tls.client_certificate_path = proxy['_client_certificate_path']; if (proxy['_client_key']) parsedProxy.tls.client_key = proxy['_client_key']; if (proxy['_client_key_path']) parsedProxy.tls.client_key_path = proxy['_client_key_path']; if (!parsedProxy.tls.enabled) delete parsedProxy.tls; }; const sshParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'ssh', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.username) parsedProxy.user = proxy.username; if (proxy.password) parsedProxy.password = proxy.password; // https://wiki.metacubex.one/config/proxies/ssh // https://sing-box.sagernet.org/zh/configuration/outbound/ssh if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey']; if (proxy['private-key']) parsedProxy.private_key_path = proxy['private-key']; if (proxy['private-key-passphrase']) parsedProxy.private_key_passphrase = proxy['private-key-passphrase']; if (proxy['server-fingerprint']) { parsedProxy.host_key = [proxy['server-fingerprint']]; // https://manual.nssurge.com/policy/ssh.html // Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.) // TODO: ? parsedProxy.host_key_algorithms = [ proxy['server-fingerprint'].split(' ')[0], ]; } if (proxy['host-key']) parsedProxy.host_key = proxy['host-key']; if (proxy['host-key-algorithms']) parsedProxy.host_key_algorithms = proxy['host-key-algorithms']; if (proxy['fast-open']) parsedProxy.udp_fragment = true; tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const httpParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'http', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), tls: { enabled: false, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.username) parsedProxy.username = proxy.username; if (proxy.password) parsedProxy.password = proxy.password; if (proxy.headers) { parsedProxy.headers = {}; for (const k of Object.keys(proxy.headers)) { parsedProxy.headers[k] = `${proxy.headers[k]}`; } if (Object.keys(parsedProxy.headers).length === 0) delete parsedProxy.headers; } if (proxy['fast-open']) parsedProxy.udp_fragment = true; tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const socks5Parser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'socks', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), version: '5', }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.username) parsedProxy.username = proxy.username; if (proxy.password) parsedProxy.password = proxy.password; if (proxy.uot) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) { parsedProxy.udp_over_tcp = { enabled: true, version: !proxy['udp-over-tcp-version'] || proxy['udp-over-tcp-version'] === 1 ? 1 : 2, }; } if (proxy['fast-open']) parsedProxy.udp_fragment = true; networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const shadowTLSParser = (proxy = {}) => { const ssPart = { tag: proxy.name, type: 'shadowsocks', method: proxy.cipher, password: proxy.password, detour: `${proxy.name}_shadowtls`, }; if (proxy.uot) ssPart.udp_over_tcp = true; if (proxy['udp-over-tcp']) { ssPart.udp_over_tcp = { enabled: true, version: !proxy['udp-over-tcp-version'] || proxy['udp-over-tcp-version'] === 1 ? 1 : 2, }; } const stPart = { tag: `${proxy.name}_shadowtls`, type: 'shadowtls', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), version: proxy['plugin-opts'].version, password: proxy['plugin-opts'].password, tls: { enabled: true, server_name: proxy['plugin-opts'].host, utls: { enabled: true, fingerprint: proxy['client-fingerprint'], }, }, }; if (stPart.server_port < 0 || stPart.server_port > 65535) throw '端口值非法'; if (proxy['fast-open'] === true) stPart.udp_fragment = true; tfoParser(proxy, stPart); detourParser(proxy, stPart); smuxParser(proxy.smux, ssPart); ipVersionParser(proxy, stPart); return { type: 'ss-with-st', ssPart, stPart }; }; const ssParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'shadowsocks', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), method: proxy.cipher, password: proxy.password, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.uot) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) { parsedProxy.udp_over_tcp = { enabled: true, version: !proxy['udp-over-tcp-version'] || proxy['udp-over-tcp-version'] === 1 ? 1 : 2, }; } if (proxy['fast-open']) parsedProxy.udp_fragment = true; networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); if (proxy.plugin) { const optArr = []; if (proxy.plugin === 'obfs') { parsedProxy.plugin = 'obfs-local'; parsedProxy.plugin_opts = ''; if (proxy['obfs-host']) proxy['plugin-opts'].host = proxy['obfs-host']; Object.keys(proxy['plugin-opts']).forEach((k) => { switch (k) { case 'mode': optArr.push(`obfs=${proxy['plugin-opts'].mode}`); break; case 'host': optArr.push(`obfs-host=${proxy['plugin-opts'].host}`); break; default: optArr.push(`${k}=${proxy['plugin-opts'][k]}`); break; } }); } if (proxy.plugin === 'v2ray-plugin') { parsedProxy.plugin = 'v2ray-plugin'; if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host']; if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path']; Object.keys(proxy['plugin-opts']).forEach((k) => { switch (k) { case 'tls': if (proxy['plugin-opts'].tls) optArr.push('tls'); break; case 'host': optArr.push(`host=${proxy['plugin-opts'].host}`); break; case 'path': optArr.push(`path=${proxy['plugin-opts'].path}`); break; case 'headers': optArr.push( `headers=${JSON.stringify( proxy['plugin-opts'].headers, )}`, ); break; case 'mux': if (proxy['plugin-opts'].mux) parsedProxy.multiplex = { enabled: true }; break; default: optArr.push(`${k}=${proxy['plugin-opts'][k]}`); } }); } parsedProxy.plugin_opts = optArr.join(';'); } return parsedProxy; }; // eslint-disable-next-line no-unused-vars const ssrParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'shadowsocksr', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), method: proxy.cipher, password: proxy.password, obfs: proxy.obfs, protocol: proxy.protocol, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param']; if (proxy['protocol-param'] && proxy['protocol-param'] !== '') parsedProxy.protocol_param = proxy['protocol-param']; if (proxy['fast-open']) parsedProxy.udp_fragment = true; tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const vmessParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'vmess', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), uuid: proxy.uuid, security: proxy.cipher, alter_id: parseInt(`${proxy.alterId}`, 10), tls: { enabled: false, server_name: proxy.server, insecure: false }, }; if ( [ 'auto', 'none', 'zero', 'aes-128-gcm', 'chacha20-poly1305', 'aes-128-ctr', ].indexOf(parsedProxy.security) === -1 ) parsedProxy.security = 'auto'; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.xudp) parsedProxy.packet_encoding = 'xudp'; if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy.network === 'ws') wsParser(proxy, parsedProxy); if (proxy.network === 'h2') h2Parser(proxy, parsedProxy); if (proxy.network === 'http') h1Parser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const vlessParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'vless', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), uuid: proxy.uuid, tls: { enabled: false, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.xudp) parsedProxy.packet_encoding = 'xudp'; if (proxy['fast-open']) parsedProxy.udp_fragment = true; // if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow; if (proxy.flow != null) parsedProxy.flow = proxy.flow; if (proxy.network === 'ws') wsParser(proxy, parsedProxy); if (proxy.network === 'h2') h2Parser(proxy, parsedProxy); if (proxy.network === 'http') h1Parser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); tlsParser(proxy, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const trojanParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'trojan', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), password: proxy.password, tls: { enabled: true, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); if (proxy.network === 'ws') wsParser(proxy, parsedProxy); networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const naiveParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'naive', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), tls: { enabled: true, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy.username) parsedProxy.username = proxy.username; if (proxy.password) parsedProxy.password = proxy.password; if (proxy.uot) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) { parsedProxy.udp_over_tcp = { enabled: true, version: !proxy['udp-over-tcp-version'] || proxy['udp-over-tcp-version'] === 1 ? 1 : 2, }; } const insecure_concurrency = parseInt( `${proxy['insecure-concurrency']}`, 10, ); if (Number.isInteger(insecure_concurrency) && insecure_concurrency >= 0) parsedProxy.insecure_concurrency = insecure_concurrency; if (proxy['extra-headers']) parsedProxy.extra_headers = proxy['extra-headers']; if (proxy.quic) parsedProxy.quic = !!proxy.quic; if (proxy['quic-congestion-control']) parsedProxy.quic_congestion_control = proxy['quic-congestion-control']; if (proxy['fast-open']) parsedProxy.udp_fragment = true; tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); if (parsedProxy.tls?.insecure) { $.info( `Platform sing-box: insecure is not supported on naive outbound`, ); delete parsedProxy.tls.insecure; } return parsedProxy; }; const hysteriaParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'hysteria', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), disable_mtu_discovery: false, tls: { enabled: true, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy['hop-interval']) parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval']) ? `${proxy['hop-interval']}s` : proxy['hop-interval']; if (proxy['ports']) parsedProxy.server_ports = proxy['ports'].split(/\s*,\s*/).map((p) => { const range = p.replace(/\s*-\s*/g, ':'); return range.includes(':') ? range : `${range}:${range}`; }); if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`; if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`; if (proxy['fast-open']) parsedProxy.udp_fragment = true; // eslint-disable-next-line no-control-regex const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$'); // sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) { parsedProxy.up = `${proxy.up}`; } else { parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10); } if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) { parsedProxy.down = `${proxy.down}`; } else { parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10); } if (proxy.obfs) parsedProxy.obfs = proxy.obfs; if (proxy.recv_window_conn) parsedProxy.recv_window_conn = proxy.recv_window_conn; if (proxy['recv-window-conn']) parsedProxy.recv_window_conn = proxy['recv-window-conn']; if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window; if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window']; if (proxy.disable_mtu_discovery) { if (typeof proxy.disable_mtu_discovery === 'boolean') { parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery; } else { if (proxy.disable_mtu_discovery === 1) parsedProxy.disable_mtu_discovery = true; } } networkParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const hysteria2Parser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'hysteria2', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), password: proxy.password, obfs: {}, tls: { enabled: true, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy['hop-interval']) parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval']) ? `${proxy['hop-interval']}s` : proxy['hop-interval']; if (proxy['ports']) parsedProxy.server_ports = proxy['ports'].split(/\s*,\s*/).map((p) => { const range = p.replace(/\s*-\s*/g, ':'); return range.includes(':') ? range : `${range}:${range}`; }); if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10); if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10); if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander'; if (proxy['obfs-password']) parsedProxy.obfs.password = proxy['obfs-password']; if (!parsedProxy.obfs.type) delete parsedProxy.obfs; networkParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const tuic5Parser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'tuic', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), uuid: proxy.uuid, password: proxy.password, tls: { enabled: true, server_name: proxy.server, insecure: false }, }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy['fast-open']) parsedProxy.udp_fragment = true; if ( proxy['congestion-controller'] && proxy['congestion-controller'] !== 'cubic' ) parsedProxy.congestion_control = proxy['congestion-controller']; if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native') parsedProxy.udp_relay_mode = proxy['udp-relay-mode']; if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true; if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true; if (proxy['heartbeat-interval']) parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`; networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const anytlsParser = (proxy = {}) => { const parsedProxy = { tag: proxy.name, type: 'anytls', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), password: proxy.password, tls: { enabled: true, server_name: proxy.server, insecure: false }, }; if (/^\d+$/.test(proxy['idle-session-check-interval'])) parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`; if (/^\d+$/.test(proxy['idle-session-timeout'])) parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`; if (/^\d+$/.test(proxy['min-idle-session'])) parsedProxy.min_idle_session = parseInt( `${proxy['min-idle-session']}`, 10, ); networkParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy); ipVersionParser(proxy, parsedProxy); return parsedProxy; }; const wireguardParser = (proxy = {}) => { const address = ['ip', 'ipv6'] .map((i) => proxy[i]) .map((i) => { if (isIPv4(i)) return `${i}/32`; if (isIPv6(i)) return `${i}/128`; }) .filter((i) => i); const parsedProxy = { system: !!proxy.system, mtu: proxy.mtu ? parseInt(`${proxy.mtu}`, 10) : undefined, udp_timeout: proxy['udp-timeout'] ? parseInt(`${proxy['udp-timeout']}`, 10) : undefined, workers: proxy['workers'] ? parseInt(`${proxy['workers']}`, 10) : undefined, tag: proxy.name, type: 'wireguard', server: proxy.server, server_port: parseInt(`${proxy.port}`, 10), address, private_key: proxy['private-key'], peer_public_key: proxy['public-key'], pre_shared_key: proxy['pre-shared-key'], reserved: [], }; if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) throw 'invalid port'; if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (typeof proxy.reserved === 'string') { parsedProxy.reserved = proxy.reserved; } else if (Array.isArray(proxy.reserved)) { for (const r of proxy.reserved) parsedProxy.reserved.push(r); } else { delete parsedProxy.reserved; } if (!Array.isArray(proxy.peers) || proxy.peers.length === 0) { proxy.peers = [{}]; } if (proxy.peers && proxy.peers.length > 0) { parsedProxy.peers = []; for (const p of proxy.peers) { let address; let port; if (p.server && p.port) { address = p.server; port = parseInt(`${p.port}`, 10); } else { address = parsedProxy.server; port = parseInt(`${parsedProxy.server_port}`, 10); } const peer = { address, port, persistent_keepalive_interval: p[ 'persistent-keepalive-interval' ] ? parseInt(`${p['persistent-keepalive-interval']}`, 10) : undefined, public_key: p['public-key'] || p['public_key'] || parsedProxy.peer_public_key, pre_shared_key: p['pre-shared-key'] || p['pre_shared_key'] || parsedProxy.pre_shared_key, allowed_ips: p['allowed-ips'] || p.allowed_ips || [ '0.0.0.0/0', ...(proxy.ipv6 ? ['::/0'] : []), ], reserved: [], }; if (typeof p.reserved === 'string') { peer.reserved.push(p.reserved); } else if (Array.isArray(p.reserved)) { for (const r of p.reserved) peer.reserved.push(r); } else { delete peer.reserved; } if (!Array.isArray(peer.reserved) || peer.reserved.length === 0) { peer.reserved = parsedProxy.reserved; } // if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key']; parsedProxy.peers.push(peer); } } networkParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy); detourParser(proxy, parsedProxy); smuxParser(proxy.smux, parsedProxy); ipVersionParser(proxy, parsedProxy); delete parsedProxy.server; delete parsedProxy.server_port; delete parsedProxy.pre_shared_key; delete parsedProxy.peer_public_key; delete parsedProxy.reserved; return parsedProxy; }; export default function singbox_Producer() { const type = 'ALL'; const produce = (proxies, type, opts = {}) => { const list = []; ClashMeta_Producer() .produce(proxies, 'internal', { 'include-unsupported-proxy': true }) .map((proxy) => { try { switch (proxy.type) { case 'ssh': list.push(sshParser(proxy)); break; case 'http': list.push(httpParser(proxy)); break; case 'socks5': if (proxy.tls) { throw new Error( `Platform sing-box does not support proxy type: ${proxy.type} with tls`, ); } else { list.push(socks5Parser(proxy)); } break; case 'ss': // if (!proxy.cipher) { // proxy.cipher = 'none'; // } // if ( // ![ // '2022-blake3-aes-128-gcm', // '2022-blake3-aes-256-gcm', // '2022-blake3-chacha20-poly1305', // 'aes-128-cfb', // 'aes-128-ctr', // 'aes-128-gcm', // 'aes-192-cfb', // 'aes-192-ctr', // 'aes-192-gcm', // 'aes-256-cfb', // 'aes-256-ctr', // 'aes-256-gcm', // 'chacha20-ietf', // 'chacha20-ietf-poly1305', // 'none', // 'rc4-md5', // 'xchacha20', // 'xchacha20-ietf-poly1305', // ].includes(proxy.cipher) // ) { // throw new Error( // `cipher ${proxy.cipher} is not supported`, // ); // } if (proxy.plugin === 'shadow-tls') { const { ssPart, stPart } = shadowTLSParser(proxy); list.push(ssPart); list.push(stPart); } else { list.push(ssParser(proxy)); } break; case 'ssr': if (opts['include-unsupported-proxy']) { list.push(ssrParser(proxy)); } else { throw new Error( `Platform sing-box does not support proxy type: ${proxy.type}`, ); } break; case 'vmess': if ( !proxy.network || ['tcp', 'ws', 'grpc', 'h2', 'http'].includes( proxy.network, ) ) { list.push(vmessParser(proxy)); } else { throw new Error( `Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`, ); } break; case 'vless': if ( proxy.encryption && proxy.encryption !== 'none' ) { throw new Error( `VLESS encryption is not supported`, ); } if ( !proxy.flow || ['xtls-rprx-vision'].includes(proxy.flow) ) { list.push(vlessParser(proxy)); } else { throw new Error( `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`, ); } break; case 'trojan': if (!proxy.flow) { list.push(trojanParser(proxy)); } else { throw new Error( `Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`, ); } break; case 'naive': list.push(naiveParser(proxy)); break; case 'hysteria': list.push(hysteriaParser(proxy)); break; case 'hysteria2': list.push( hysteria2Parser( proxy, opts['include-unsupported-proxy'], ), ); break; case 'tuic': if (!proxy.token || proxy.token.length === 0) { list.push(tuic5Parser(proxy)); } else { throw new Error( `Platform sing-box does not support proxy type: TUIC v4`, ); } break; case 'wireguard': list.push(wireguardParser(proxy)); break; case 'anytls': list.push(anytlsParser(proxy)); break; default: throw new Error( `Platform sing-box does not support proxy type: ${proxy.type}`, ); } } catch (e) { // console.log(e); $.error(e.message ?? e); } }); if (type === 'internal') return list; const categorized = list.reduce( (result, item) => { if (['wireguard'].includes(item.type)) { result.endpoints.push(item); } else { result.outbounds.push(item); } return result; }, { outbounds: [], endpoints: [] }, ); return JSON.stringify(categorized, null, 2); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/stash.js ================================================ import { isPresent } from '@/core/proxy-utils/producers/utils'; import $ from '@/core/app'; export default function Stash_Producer() { const type = 'ALL'; const produce = (proxies, type, opts = {}) => { // https://stash.wiki/proxy-protocols/proxy-types#shadowsocks const list = proxies .filter((proxy) => { if ( ![ 'ss', 'ssr', 'vmess', 'socks5', 'http', 'snell', 'trojan', 'tuic', 'vless', 'wireguard', 'hysteria', 'hysteria2', 'ssh', 'juicity', 'anytls', ].includes(proxy.type) || (proxy.type === 'ss' && ![ 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'rc4-md5', 'chacha20-ietf', 'xchacha20', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', ].includes(proxy.cipher)) || (proxy.type === 'snell' && proxy.version >= 4) || (proxy.type === 'vless' && proxy['reality-opts'] && !['xtls-rprx-vision'].includes(proxy.flow)) ) { return false; } else if ( ['anytls'].includes(proxy.type) && proxy.network && (!['tcp'].includes(proxy.network) || (['tcp'].includes(proxy.network) && proxy['reality-opts'])) ) { return false; } else if (['xhttp'].includes(proxy.network)) { return false; } else if ( proxy.encryption && proxy.encryption !== 'none' && ['vless'].includes(proxy.type) ) { return false; } else if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { return false; } return true; }) .map((proxy) => { if (proxy.type === 'vmess') { // handle vmess aead if (isPresent(proxy, 'aead')) { if (proxy.aead) { proxy.alterId = 0; } delete proxy.aead; } if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400 // https://stash.wiki/proxy-protocols/proxy-types#vmess if ( isPresent(proxy, 'cipher') && ![ 'auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', ].includes(proxy.cipher) ) { proxy.cipher = 'auto'; } } else if (proxy.type === 'tuic') { if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } else { proxy.alpn = ['h3']; } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; delete proxy.tfo; } // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 if ( (!proxy.token || proxy.token.length === 0) && !isPresent(proxy, 'version') ) { proxy.version = 5; } } else if (proxy.type === 'hysteria') { // auth_str 将会在未来某个时候删除 但是有的机场不规范 if ( isPresent(proxy, 'auth_str') && !isPresent(proxy, 'auth-str') ) { proxy['auth-str'] = proxy['auth_str']; } if (isPresent(proxy, 'alpn')) { proxy.alpn = Array.isArray(proxy.alpn) ? proxy.alpn : [proxy.alpn]; } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; delete proxy.tfo; } if ( isPresent(proxy, 'down') && !isPresent(proxy, 'down-speed') ) { proxy['down-speed'] = proxy.down; delete proxy.down; } if ( isPresent(proxy, 'up') && !isPresent(proxy, 'up-speed') ) { proxy['up-speed'] = proxy.up; delete proxy.up; } if (isPresent(proxy, 'down-speed')) { proxy['down-speed'] = `${proxy['down-speed']}`.match(/\d+/)?.[0] || 0; } if (isPresent(proxy, 'up-speed')) { proxy['up-speed'] = `${proxy['up-speed']}`.match(/\d+/)?.[0] || 0; } } else if (proxy.type === 'hysteria2') { if ( isPresent(proxy, 'password') && !isPresent(proxy, 'auth') ) { proxy.auth = proxy.password; delete proxy.password; } if ( isPresent(proxy, 'tfo') && !isPresent(proxy, 'fast-open') ) { proxy['fast-open'] = proxy.tfo; delete proxy.tfo; } if ( isPresent(proxy, 'down') && !isPresent(proxy, 'down-speed') ) { proxy['down-speed'] = proxy.down; delete proxy.down; } if ( isPresent(proxy, 'up') && !isPresent(proxy, 'up-speed') ) { proxy['up-speed'] = proxy.up; delete proxy.up; } if (isPresent(proxy, 'down-speed')) { proxy['down-speed'] = `${proxy['down-speed']}`.match(/\d+/)?.[0] || 0; } if (isPresent(proxy, 'up-speed')) { proxy['up-speed'] = `${proxy['up-speed']}`.match(/\d+/)?.[0] || 0; } } else if (proxy.type === 'wireguard') { proxy.keepalive = proxy.keepalive ?? proxy['persistent-keepalive']; proxy['persistent-keepalive'] = proxy.keepalive; proxy['preshared-key'] = proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['pre-shared-key'] = proxy['preshared-key']; } else if (proxy.type === 'snell' && proxy.version < 3) { delete proxy.udp; } else if (proxy.type === 'vless') { if (isPresent(proxy, 'sni')) { proxy.servername = proxy.sni; delete proxy.sni; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http' ) { let httpPath = proxy['http-opts']?.path; if ( isPresent(proxy, 'http-opts.path') && !Array.isArray(httpPath) ) { proxy['http-opts'].path = [httpPath]; } let httpHost = proxy['http-opts']?.headers?.Host; if ( isPresent(proxy, 'http-opts.headers.Host') && !Array.isArray(httpHost) ) { proxy['http-opts'].headers.Host = [httpHost]; } } if ( ['vmess', 'vless'].includes(proxy.type) && proxy.network === 'h2' ) { let path = proxy['h2-opts']?.path; if ( isPresent(proxy, 'h2-opts.path') && Array.isArray(path) ) { proxy['h2-opts'].path = path[0]; } let host = proxy['h2-opts']?.headers?.host; if ( isPresent(proxy, 'h2-opts.headers.Host') && !Array.isArray(host) ) { proxy['h2-opts'].headers.host = [host]; } } if (['ws'].includes(proxy.network)) { const networkPath = proxy[`${proxy.network}-opts`]?.path; if (networkPath) { const reg = /^(.*?)(?:\?ed=(\d+))?$/; // eslint-disable-next-line no-unused-vars const [_, path = '', ed = ''] = reg.exec(networkPath); proxy[`${proxy.network}-opts`].path = path; if (ed !== '') { proxy['ws-opts']['early-data-header-name'] = 'Sec-WebSocket-Protocol'; proxy['ws-opts']['max-early-data'] = parseInt( ed, 10, ); } } else { proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {}; proxy[`${proxy.network}-opts`].path = '/'; } } if (proxy['plugin-opts']?.tls) { if (isPresent(proxy, 'skip-cert-verify')) { proxy['plugin-opts']['skip-cert-verify'] = proxy['skip-cert-verify']; } } if ( [ 'trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity', 'anytls', 'trusttunnel', 'naive', ].includes(proxy.type) ) { delete proxy.tls; } if (proxy['tls-fingerprint']) { proxy['server-cert-fingerprint'] = proxy['tls-fingerprint']; } delete proxy['tls-fingerprint']; if (proxy['underlying-proxy']) { proxy['dialer-proxy'] = proxy['underlying-proxy']; } delete proxy['underlying-proxy']; if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { delete proxy.tls; } if (proxy['test-url']) { proxy['benchmark-url'] = proxy['test-url']; delete proxy['test-url']; } if (proxy['test-timeout']) { proxy['benchmark-timeout'] = proxy['test-timeout']; delete proxy['test-timeout']; } delete proxy.subName; delete proxy.collectionName; delete proxy.id; delete proxy.resolved; delete proxy['no-resolve']; if (type !== 'internal') { for (const key in proxy) { if (proxy[key] == null || /^_/i.test(key)) { delete proxy[key]; } } } if ( ['grpc'].includes(proxy.network) && proxy[`${proxy.network}-opts`] ) { delete proxy[`${proxy.network}-opts`]['_grpc-type']; delete proxy[`${proxy.network}-opts`]['_grpc-authority']; } return proxy; }); return type === 'internal' ? list : 'proxies:\n' + list .map((proxy) => ' - ' + JSON.stringify(proxy) + '\n') .join(''); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/surfboard.js ================================================ import { Result, isPresent } from './utils'; import { isNotBlank } from '@/utils'; // import $ from '@/core/app'; const targetPlatform = 'Surfboard'; export default function Surfboard_Producer() { const produce = (proxy) => { if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { throw new Error( `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`, ); } proxy.name = proxy.name.replace(/=|,/g, ''); switch (proxy.type) { case 'ss': return shadowsocks(proxy); case 'trojan': return trojan(proxy); case 'vmess': return vmess(proxy); case 'http': return http(proxy); case 'snell': return snell(proxy); case 'socks5': return socks5(proxy); case 'anytls': return anytls(proxy); case 'wireguard-surge': return wireguard(proxy); } throw new Error( `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, ); }; return { produce }; } function anytls(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // reuse result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); return result.toString(); } function snell(proxy) { if (proxy.version > 3) { throw new Error( `Platform ${targetPlatform} does not support snell version ${proxy.version}`, ); } const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,version=${proxy.version}`, 'version'); result.appendIfPresent(`,psk=${proxy.psk}`, 'psk'); // obfs result.appendIfPresent( `,obfs=${proxy['obfs-opts']?.mode}`, 'obfs-opts.mode', ); result.appendIfPresent( `,obfs-host=${proxy['obfs-opts']?.host}`, 'obfs-opts.host', ); result.appendIfPresent( `,obfs-uri=${proxy['obfs-opts']?.path}`, 'obfs-opts.path', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp if (proxy.version >= 3) { result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); } return result.toString(); } function shadowsocks(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); if ( ![ 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', 'rc4', 'rc4-md5', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'bf-cfb', 'camellia-128-cfb', 'camellia-192-cfb', 'camellia-256-cfb', 'salsa20', 'chacha20', 'chacha20-ietf', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', ].includes(proxy.cipher) ) { throw new Error(`cipher ${proxy.cipher} is not supported`); } result.append(`,encrypt-method=${proxy.cipher}`); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); // obfs if (isPresent(proxy, 'plugin')) { if (proxy.plugin === 'obfs') { result.append(`,obfs=${proxy['plugin-opts'].mode}`); result.appendIfPresent( `,obfs-host=${proxy['plugin-opts'].host}`, 'plugin-opts.host', ); result.appendIfPresent( `,obfs-uri=${proxy['plugin-opts'].path}`, 'plugin-opts.path', ); } else { throw new Error(`plugin ${proxy.plugin} is not supported`); } } // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); return result.toString(); } function trojan(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,password=${proxy.password}`, 'password'); // transport handleTransport(result, proxy); // tls result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); return result.toString(); } function vmess(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); // transport handleTransport(result, proxy); // AEAD if (isPresent(proxy, 'aead')) { result.append(`,vmess-aead=${proxy.aead}`); } else { result.append(`,vmess-aead=${proxy.alterId === 0}`); } // tls result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); return result.toString(); } function http(proxy) { const result = new Result(proxy); const type = proxy.tls ? 'https' : 'http'; result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.password}`, 'password'); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); return result.toString(); } function socks5(proxy) { const result = new Result(proxy); const type = proxy.tls ? 'socks5-tls' : 'socks5'; result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.password}`, 'password'); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); return result.toString(); } function wireguard(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=wireguard`); result.appendIfPresent( `,section-name=${proxy['section-name']}`, 'section-name', ); return result.toString(); } function handleTransport(result, proxy) { if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { result.append(`,ws=true`); if (isPresent(proxy, 'ws-opts')) { result.appendIfPresent( `,ws-path=${proxy['ws-opts'].path}`, 'ws-opts.path', ); if (isPresent(proxy, 'ws-opts.headers')) { const headers = proxy['ws-opts'].headers; const value = Object.keys(headers) .map((k) => { let v = headers[k]; if (['Host'].includes(k)) { v = `"${v}"`; } return `${k}:${v}`; }) .join('|'); if (isNotBlank(value)) { result.append(`,ws-headers=${value}`); } } } } else if (['tcp'].includes(proxy.network) && proxy['reality-opts']) { throw new Error(`reality is unsupported`); } else if (!['tcp'].includes(proxy.network)) { throw new Error(`network ${proxy.network} is unsupported`); } } } ================================================ FILE: backend/src/core/proxy-utils/producers/surge.js ================================================ import { Result, isPresent } from './utils'; import { isNotBlank, getIfNotBlank } from '@/utils'; import $ from '@/core/app'; const targetPlatform = 'Surge'; const ipVersions = { dual: 'dual', ipv4: 'v4-only', ipv6: 'v6-only', 'ipv4-prefer': 'prefer-v4', 'ipv6-prefer': 'prefer-v6', }; export default function Surge_Producer() { const produce = (proxy, type, opts = {}) => { if ( ['ws'].includes(proxy.network) && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { throw new Error( `Platform ${targetPlatform} does not support network ${proxy.network} with http upgrade`, ); } proxy.name = proxy.name.replace(/=|,/g, ''); if (proxy.ports) { proxy.ports = String(proxy.ports); } switch (proxy.type) { case 'ss': return shadowsocks(proxy); case 'trojan': return trojan(proxy); case 'vmess': return vmess(proxy, opts['include-unsupported-proxy']); case 'http': return http(proxy); case 'direct': return direct(proxy); case 'socks5': return socks5(proxy); case 'snell': return snell(proxy); case 'tuic': return tuic(proxy); case 'wireguard-surge': return wireguard_surge(proxy); case 'hysteria2': return hysteria2(proxy, opts['include-unsupported-proxy']); case 'ssh': return ssh(proxy); } if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') { return wireguard(proxy); } if (opts['include-unsupported-proxy'] && proxy.type === 'anytls') { if ( proxy.network && (!['tcp'].includes(proxy.network) || (['tcp'].includes(proxy.network) && proxy['reality-opts'])) ) { throw new Error( `Platform ${targetPlatform} does not support proxy type ${proxy.type} with network or reality`, ); } return anytls(proxy); } if (opts['include-unsupported-proxy'] && proxy.type === 'trusttunnel') { return trusttunnel(proxy); } throw new Error( `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, ); }; return { produce }; } function shadowsocks(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); if (!proxy.cipher) { proxy.cipher = 'none'; } if ( ![ 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305', 'xchacha20-ietf-poly1305', 'rc4', 'rc4-md5', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'bf-cfb', 'camellia-128-cfb', 'camellia-192-cfb', 'camellia-256-cfb', 'cast5-cfb', 'des-cfb', 'idea-cfb', 'rc2-cfb', 'seed-cfb', 'salsa20', 'chacha20', 'chacha20-ietf', 'none', '2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm', ].includes(proxy.cipher) ) { throw new Error(`cipher ${proxy.cipher} is not supported`); } result.append(`,encrypt-method=${proxy.cipher}`); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // obfs if (isPresent(proxy, 'plugin')) { if (proxy.plugin === 'obfs') { result.append(`,obfs=${proxy['plugin-opts'].mode}`); result.appendIfPresent( `,obfs-host=${proxy['plugin-opts'].host}`, 'plugin-opts.host', ); result.appendIfPresent( `,obfs-uri=${proxy['plugin-opts'].path}`, 'plugin-opts.path', ); } else if (!['shadow-tls'].includes(proxy.plugin)) { throw new Error(`plugin ${proxy.plugin} is not supported`); } } // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); // udp-port result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port'); } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) { const password = proxy['plugin-opts'].password; const host = proxy['plugin-opts'].host; const version = proxy['plugin-opts'].version; if (password) { result.append(`,shadow-tls-password=${password}`); if (host) { result.append(`,shadow-tls-sni=${host}`); } if (version) { if (version < 2) { throw new Error( `shadow-tls version ${version} is not supported`, ); } result.append(`,shadow-tls-version=${version}`); } // udp-port result.appendIfPresent( `,udp-port=${proxy['udp-port']}`, 'udp-port', ); } } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function trojan(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // transport handleTransport(result, proxy); // tls result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function anytls(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); // reuse result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); return result.toString(); } function trusttunnel(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=trust-tunnel,${proxy.server},${proxy.port}`); result.appendIfPresent(`,username="${proxy.username}"`, 'username'); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); // reuse result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); return result.toString(); } function vmess(proxy, includeUnsupportedProxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // transport handleTransport(result, proxy, includeUnsupportedProxy); // AEAD if (isPresent(proxy, 'aead')) { result.append(`,vmess-aead=${proxy.aead}`); } else { result.append(`,vmess-aead=${proxy.alterId === 0}`); } // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls result.appendIfPresent(`,tls=${proxy.tls}`, 'tls'); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function ssh(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`); result.appendIfPresent(`,username="${proxy.username}"`, 'username'); // 所有的类似的字段都有双引号的问题 暂不处理 result.appendIfPresent(`,password="${proxy.password}"`, 'password'); // https://manual.nssurge.com/policy/ssh.html // 需配合 Keystore result.appendIfPresent( `,private-key=${proxy['keystore-private-key']}`, 'keystore-private-key', ); result.appendIfPresent( `,idle-timeout=${proxy['idle-timeout']}`, 'idle-timeout', ); result.appendIfPresent( `,server-fingerprint="${proxy['server-fingerprint']}"`, 'server-fingerprint', ); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function http(proxy) { if (proxy.headers && Object.keys(proxy.headers).length > 0) { throw new Error(`headers is unsupported`); } const result = new Result(proxy); const type = proxy.tls ? 'https' : 'http'; result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,username="${proxy.username}"`, 'username'); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function direct(proxy) { const result = new Result(proxy); const type = 'direct'; result.append(`${proxy.name}=${type}`); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function socks5(proxy) { const result = new Result(proxy); const type = proxy.tls ? 'socks5-tls' : 'socks5'; result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,username="${proxy.username}"`, 'username'); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tfo if (proxy.tfo) { $.info(`Option tfo is not supported by Surge, thus omitted`); } // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function snell(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,version=${proxy.version}`, 'version'); result.appendIfPresent(`,psk=${proxy.psk}`, 'psk'); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // obfs result.appendIfPresent( `,obfs=${proxy['obfs-opts']?.mode}`, 'obfs-opts.mode', ); result.appendIfPresent( `,obfs-host=${proxy['obfs-opts']?.host}`, 'obfs-opts.host', ); result.appendIfPresent( `,obfs-uri=${proxy['obfs-opts']?.path}`, 'obfs-opts.path', ); // tfo result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); // reuse result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse'); return result.toString(); } function tuic(proxy) { const result = new Result(proxy); // https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197 let type = proxy.type; if (!proxy.token || proxy.token.length === 0) { type = 'tuic-v5'; } result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid'); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); result.appendIfPresent(`,token=${proxy.token}`, 'token'); result.appendIfPresent( `,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`, 'alpn', ); if (isPresent(proxy, 'ports')) { result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`); } result.appendIfPresent( `,port-hopping-interval=${proxy['hop-interval']}`, 'hop-interval', ); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); // tls fingerprint result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tfo if (isPresent(proxy, 'tfo')) { result.append(`,tfo=${proxy['tfo']}`); } else if (isPresent(proxy, 'fast-open')) { result.append(`,tfo=${proxy['fast-open']}`); } // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn'); return result.toString(); } function wireguard(proxy) { if (Array.isArray(proxy.peers) && proxy.peers.length > 0) { proxy.server = proxy.peers[0].server; proxy.port = proxy.peers[0].port; proxy.ip = proxy.peers[0].ip; proxy.ipv6 = proxy.peers[0].ipv6; proxy['public-key'] = proxy.peers[0]['public-key']; proxy['preshared-key'] = proxy.peers[0]['pre-shared-key']; // https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717 proxy['allowed-ips'] = proxy.peers[0]['allowed-ips']; proxy.reserved = proxy.peers[0].reserved; } const result = new Result(proxy); result.append(`# > WireGuard Proxy ${proxy.name} # ${proxy.name}=wireguard`); proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name); result.appendIfPresent( `,section-name=${proxy['section-name']}`, 'section-name', ); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); result.append(` # > WireGuard Section ${proxy.name} [WireGuard ${proxy['section-name']}] private-key = ${proxy['private-key']}`); result.appendIfPresent(`\nself-ip = ${proxy.ip}`, 'ip'); result.appendIfPresent(`\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6'); if (proxy.dns) { if (Array.isArray(proxy.dns)) { proxy.dns = proxy.dns.join(', '); } result.append(`\ndns-server = ${proxy.dns}`); } result.appendIfPresent(`\nmtu = ${proxy.mtu}`, 'mtu'); if (ip_version === 'prefer-v6') { result.append(`\nprefer-ipv6 = true`); } const allowedIps = Array.isArray(proxy['allowed-ips']) ? proxy['allowed-ips'].join(',') : proxy['allowed-ips']; let reserved = Array.isArray(proxy.reserved) ? proxy.reserved.join('/') : proxy.reserved; let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key']; const peer = { 'public-key': proxy['public-key'], 'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined, endpoint: `${proxy.server}:${proxy.port}`, keepalive: proxy['persistent-keepalive'] || proxy.keepalive, 'client-id': reserved, 'preshared-key': presharedKey, }; result.append( `\npeer = (${Object.keys(peer) .filter((k) => peer[k] != null) .map((k) => `${k} = ${peer[k]}`) .join(', ')})`, ); return result.toString(); } function wireguard_surge(proxy) { const result = new Result(proxy); result.append(`${proxy.name}=wireguard`); result.appendIfPresent( `,section-name=${proxy['section-name']}`, 'section-name', ); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); return result.toString(); } function hysteria2(proxy, includeUnsupportedProxy) { if (includeUnsupportedProxy) { if (proxy['obfs-password'] && proxy.obfs != 'salamander') { throw new Error(`only salamander obfs is supported`); } } else { if (proxy.obfs || proxy['obfs-password']) { throw new Error(`obfs is unsupported`); } } const result = new Result(proxy); result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`); result.appendIfPresent(`,password="${proxy.password}"`, 'password'); if (isPresent(proxy, 'ports')) { result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`); } result.appendIfPresent( `,port-hopping-interval=${proxy['hop-interval']}`, 'hop-interval', ); if (proxy['obfs-password'] && proxy.obfs == 'salamander') { result.append(`,salamander-password="${proxy['obfs-password']}"`); } const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // tls verification result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni'); result.appendIfPresent( `,skip-cert-verify=${proxy['skip-cert-verify']}`, 'skip-cert-verify', ); result.appendIfPresent( `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, 'tls-fingerprint', ); // tfo if (isPresent(proxy, 'tfo')) { result.append(`,tfo=${proxy['tfo']}`); } else if (isPresent(proxy, 'fast-open')) { result.append(`,tfo=${proxy['fast-open']}`); } // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent( `,test-timeout=${proxy['test-timeout']}`, 'test-timeout', ); result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp'); result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid'); result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos'); result.appendIfPresent( `,allow-other-interface=${proxy['allow-other-interface']}`, 'allow-other-interface', ); result.appendIfPresent( `,interface=${proxy['interface-name']}`, 'interface-name', ); result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface'); // shadow-tls if (isPresent(proxy, 'shadow-tls-password')) { result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`); result.appendIfPresent( `,shadow-tls-version=${proxy['shadow-tls-version']}`, 'shadow-tls-version', ); result.appendIfPresent( `,shadow-tls-sni=${proxy['shadow-tls-sni']}`, 'shadow-tls-sni', ); } // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); // underlying-proxy result.appendIfPresent( `,underlying-proxy=${proxy['underlying-proxy']}`, 'underlying-proxy', ); // download-bandwidth result.appendIfPresent( `,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`, 'down', ); result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn'); return result.toString(); } function handleTransport(result, proxy, includeUnsupportedProxy) { if (isPresent(proxy, 'network')) { if (proxy.network === 'ws') { result.append(`,ws=true`); if (isPresent(proxy, 'ws-opts')) { result.appendIfPresent( `,ws-path=${proxy['ws-opts'].path}`, 'ws-opts.path', ); if (isPresent(proxy, 'ws-opts.headers')) { const headers = proxy['ws-opts'].headers; const value = Object.keys(headers) .map((k) => { let v = headers[k]; // if (['Host'].includes(k)) { v = `"${v}"`; // } return `${k}:${v}`; }) .join('|'); if (isNotBlank(value)) { result.append(`,ws-headers=${value}`); } } } } else { if (includeUnsupportedProxy && ['http'].includes(proxy.network)) { $.info( `Include Unsupported Proxy: network ${proxy.network} -> tcp`, ); } else if ( ['tcp'].includes(proxy.network) && proxy['reality-opts'] ) { throw new Error(`reality is unsupported`); } else if (!['tcp'].includes(proxy.network)) { throw new Error(`network ${proxy.network} is unsupported`); } } } } ================================================ FILE: backend/src/core/proxy-utils/producers/surgemac.js ================================================ import { Base64 } from 'js-base64'; import { Result, isPresent } from './utils'; import Surge_Producer from './surge'; import ClashMeta_Producer from './clashmeta'; import { isIPv4, isIPv6 } from '@/utils'; import $ from '@/core/app'; const targetPlatform = 'SurgeMac'; const surge_Producer = Surge_Producer(); export default function SurgeMac_Producer() { const produce = (proxy, type, opts = {}) => { switch (proxy.type) { case 'external': return external(proxy); // case 'ssr': // return shadowsocksr(proxy); default: { try { return surge_Producer.produce(proxy, type, opts); } catch (e) { if (opts.useMihomoExternal) { $.log( `${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`, ); return mihomo(proxy, type, opts); } else { throw new Error( `Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`, ); } } } } }; return { produce }; } function external(proxy) { const result = new Result(proxy); if (!proxy.exec || !proxy['local-port']) { throw new Error(`${proxy.type}: exec and local-port are required`); } result.append( `${proxy.name}=external,exec="${proxy.exec}",local-port=${proxy['local-port']}`, ); if (Array.isArray(proxy.args)) { proxy.args.map((args) => { result.append(`,args="${args}"`); }); } if (Array.isArray(proxy.addresses)) { proxy.addresses.map((addresses) => { result.append(`,addresses=${addresses}`); }); } result.appendIfPresent( `,no-error-alert=${proxy['no-error-alert']}`, 'no-error-alert', ); // udp result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); // tfo if (isPresent(proxy, 'tfo')) { result.append(`,tfo=${proxy['tfo']}`); } else if (isPresent(proxy, 'fast-open')) { result.append(`,tfo=${proxy['fast-open']}`); } // test-url result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); // block-quic result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic'); return result.toString(); } // eslint-disable-next-line no-unused-vars function shadowsocksr(proxy) { const external_proxy = { ...proxy, type: 'external', exec: proxy.exec || '/usr/local/bin/ssr-local', 'local-port': '__SubStoreLocalPort__', args: [], addresses: [], 'local-address': proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1', }; // https://manual.nssurge.com/policy/external-proxy.html if (isIP(proxy.server)) { external_proxy.addresses.push(proxy.server); } else { $.log( `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`, ); } for (const [key, value] of Object.entries({ cipher: '-m', obfs: '-o', 'obfs-param': '-g', password: '-k', port: '-p', protocol: '-O', 'protocol-param': '-G', server: '-s', 'local-port': '-l', 'local-address': '-b', })) { if (external_proxy[key] != null) { external_proxy.args.push(value); external_proxy.args.push(external_proxy[key]); } } return external(external_proxy); } // eslint-disable-next-line no-unused-vars function mihomo(proxy, type, opts) { const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0]; if (clashProxy) { const localPort = opts?.localPort || proxy._localPort || 65535; const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version']) ? false : true; const external_proxy = { name: proxy.name, type: 'external', udp: true, exec: proxy._exec || '/usr/local/bin/mihomo', 'local-port': localPort, args: [ '-config', Base64.encode( JSON.stringify({ 'mixed-port': localPort, ipv6, mode: 'global', dns: { enable: true, ipv6, 'default-nameserver': opts?.defaultNameserver || proxy._defaultNameserver || [ '180.76.76.76', '52.80.52.52', '119.28.28.28', '223.6.6.6', ], nameserver: opts?.nameserver || proxy._nameserver || [ 'https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query', 'https://doh-pure.onedns.net/dns-query', ], }, proxies: [ { ...clashProxy, name: 'proxy', }, ], 'proxy-groups': [ { name: 'GLOBAL', type: 'select', proxies: ['proxy'], }, ], }), ), ], addresses: [], }; // https://manual.nssurge.com/policy/external-proxy.html if (isIP(proxy.server)) { external_proxy.addresses.push(proxy.server); } else { $.log( `Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`, ); } opts.localPort = localPort - 1; return external(external_proxy); } } function isIP(ip) { return isIPv4(ip) || isIPv6(ip); } ================================================ FILE: backend/src/core/proxy-utils/producers/uri.js ================================================ /* eslint-disable no-case-declarations */ import { Base64 } from 'js-base64'; import { isIPv6 } from '@/utils'; function vless(proxy) { let security = 'none'; const isReality = proxy['reality-opts']; let sid = ''; let pbk = ''; let spx = ''; if (isReality) { security = 'reality'; const publicKey = proxy['reality-opts']?.['public-key']; if (publicKey) { pbk = `&pbk=${encodeURIComponent(publicKey)}`; } const shortId = proxy['reality-opts']?.['short-id']; if (shortId) { sid = `&sid=${encodeURIComponent(shortId)}`; } const spiderX = proxy['reality-opts']?.['_spider-x']; if (spiderX) { spx = `&spx=${encodeURIComponent(spiderX)}`; } } else if (proxy.tls) { security = 'tls'; } let alpn = ''; if (proxy.alpn) { alpn = `&alpn=${encodeURIComponent( Array.isArray(proxy.alpn) ? proxy.alpn : proxy.alpn.join(','), )}`; } let allowInsecure = ''; if (proxy['skip-cert-verify']) { allowInsecure = `&allowInsecure=1`; } let h2 = ''; if (proxy._h2) { h2 = `&h2=1`; } let pcs = ''; if (proxy._pcs) { pcs = `&pcs=${encodeURIComponent(proxy._pcs)}`; } let ech = ''; if (proxy._echConfigList) { ech = `&ech=${encodeURIComponent(proxy._echConfigList)}`; } let sni = ''; if (proxy.sni) { sni = `&sni=${encodeURIComponent(proxy.sni)}`; } let fp = ''; if (proxy['client-fingerprint']) { fp = `&fp=${encodeURIComponent(proxy['client-fingerprint'])}`; } let flow = ''; if (proxy.flow) { flow = `&flow=${encodeURIComponent(proxy.flow)}`; } let extra = ''; if (proxy._extra) { extra = `&extra=${encodeURIComponent(proxy._extra)}`; } let mode = ''; if (proxy._mode) { mode = `&mode=${encodeURIComponent(proxy._mode)}`; } let pqv = ''; if (proxy._pqv) { pqv = `&pqv=${encodeURIComponent(proxy._pqv)}`; } let encryption = ''; if (proxy.encryption) { encryption = `&encryption=${encodeURIComponent(proxy.encryption)}`; } let vlessType = proxy.network; if (proxy.network === 'ws' && proxy['ws-opts']?.['v2ray-http-upgrade']) { vlessType = 'httpupgrade'; } let vlessTransport = `&type=${encodeURIComponent(vlessType)}`; if (['grpc'].includes(proxy.network)) { // https://github.com/XTLS/Xray-core/issues/91 vlessTransport += `&mode=${encodeURIComponent( proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun', )}`; const authority = proxy[`${proxy.network}-opts`]?.['_grpc-authority']; if (authority) { vlessTransport += `&authority=${encodeURIComponent(authority)}`; } } let vlessTransportServiceName = proxy[`${proxy.network}-opts`]?.[`${proxy.network}-service-name`]; let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path; let vlessTransportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; if (vlessTransportPath) { vlessTransport += `&path=${encodeURIComponent( Array.isArray(vlessTransportPath) ? vlessTransportPath[0] : vlessTransportPath, )}`; } if (vlessTransportHost) { vlessTransport += `&host=${encodeURIComponent( Array.isArray(vlessTransportHost) ? vlessTransportHost[0] : vlessTransportHost, )}`; } if (vlessTransportServiceName) { vlessTransport += `&serviceName=${encodeURIComponent( vlessTransportServiceName, )}`; } if (proxy.network === 'kcp') { if (proxy.seed) { vlessTransport += `&seed=${encodeURIComponent(proxy.seed)}`; } if (proxy.headerType) { vlessTransport += `&headerType=${encodeURIComponent( proxy.headerType, )}`; } } return `vless://${proxy.uuid}@${proxy.server}:${ proxy.port }?security=${encodeURIComponent( security, )}${vlessTransport}${alpn}${allowInsecure}${pcs}${ech}${h2}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}${pqv}${encryption}#${encodeURIComponent( proxy.name, )}`; } export default function URI_Producer() { const type = 'SINGLE'; const produce = (proxy) => { let result = ''; delete proxy.subName; delete proxy.collectionName; delete proxy.id; delete proxy.resolved; delete proxy['no-resolve']; for (const key in proxy) { if (proxy[key] == null) { delete proxy[key]; } } if ( [ 'tuic', 'hysteria', 'hysteria2', 'juicity', 'trusttunnel', ].includes(proxy.type) ) { delete proxy.tls; } if ( !['vmess'].includes(proxy.type) && proxy.server && isIPv6(proxy.server) ) { proxy.server = `[${proxy.server}]`; } switch (proxy.type) { case 'socks5': result = `socks://${encodeURIComponent( Base64.encode( `${proxy.username ?? ''}:${proxy.password ?? ''}`, ), )}@${proxy.server}:${proxy.port}#${proxy.name}`; break; case 'ss': const userinfo = `${proxy.cipher}:${proxy.password}`; result = `ss://${ proxy.cipher?.startsWith('2022-blake3-') ? `${encodeURIComponent( proxy.cipher, )}:${encodeURIComponent(proxy.password)}` : Base64.encode(userinfo) }@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`; let query = ''; if (proxy.plugin) { query += '&plugin='; const opts = proxy['plugin-opts']; switch (proxy.plugin) { case 'obfs': query += encodeURIComponent( `simple-obfs;obfs=${opts.mode}${ opts.host ? ';obfs-host=' + opts.host : '' }`, ); break; case 'v2ray-plugin': query += encodeURIComponent( `v2ray-plugin;obfs=${opts.mode}${ opts.host ? ';obfs-host=' + opts.host : '' }${opts.host ? ';host=' + opts.host : ''}${ opts.path ? ';path=' + opts.path : '' }${opts.tls ? ';tls' : ''}`, ); break; case 'shadow-tls': query += encodeURIComponent( `shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`, ); break; default: throw new Error( `Unsupported plugin option: ${proxy.plugin}`, ); } } if (proxy['udp-over-tcp']) { query += '&uot=1'; } if (proxy.tfo) { query += '&tfo=1'; } let ssTransport = ''; if (proxy.network) { let ssType = proxy.network; if ( proxy.network === 'ws' && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { ssType = 'httpupgrade'; } ssTransport = `&type=${encodeURIComponent(ssType)}`; if (['grpc'].includes(proxy.network)) { let ssTransportServiceName = proxy[`${proxy.network}-opts`]?.[ `${proxy.network}-service-name` ]; let ssTransportAuthority = proxy[`${proxy.network}-opts`]?.['_grpc-authority']; if (ssTransportServiceName) { ssTransport += `&serviceName=${encodeURIComponent( ssTransportServiceName, )}`; } if (ssTransportAuthority) { ssTransport += `&authority=${encodeURIComponent( ssTransportAuthority, )}`; } ssTransport += `&mode=${encodeURIComponent( proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun', )}`; } let ssTransportPath = proxy[`${proxy.network}-opts`]?.path; let ssTransportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; if (ssTransportPath) { ssTransport += `&path=${encodeURIComponent( Array.isArray(ssTransportPath) ? ssTransportPath[0] : ssTransportPath, )}`; } if (ssTransportHost) { ssTransport += `&host=${encodeURIComponent( Array.isArray(ssTransportHost) ? ssTransportHost[0] : ssTransportHost, )}`; } } let ssFp = ''; if (proxy['client-fingerprint']) { ssFp = `&fp=${encodeURIComponent( proxy['client-fingerprint'], )}`; } let ssAlpn = ''; if (proxy.alpn) { ssAlpn = `&alpn=${encodeURIComponent( Array.isArray(proxy.alpn) ? proxy.alpn : proxy.alpn.join(','), )}`; } const ssIsReality = proxy['reality-opts']; let ssSid = ''; let ssPbk = ''; let ssSpx = ''; let ssSecurity = proxy.tls ? '&security=tls' : ''; let ssMode = ''; let ssExtra = ''; if (ssIsReality) { ssSecurity = `&security=reality`; const publicKey = proxy['reality-opts']?.['public-key']; if (publicKey) { ssPbk = `&pbk=${encodeURIComponent(publicKey)}`; } const shortId = proxy['reality-opts']?.['short-id']; if (shortId) { ssSid = `&sid=${encodeURIComponent(shortId)}`; } const spiderX = proxy['reality-opts']?.['_spider-x']; if (spiderX) { ssSpx = `&spx=${encodeURIComponent(spiderX)}`; } if (proxy._extra) { ssExtra = `&extra=${encodeURIComponent(proxy._extra)}`; } if (proxy._mode) { ssMode = `&mode=${encodeURIComponent(proxy._mode)}`; } } if (proxy.tls) { query += `&sni=${encodeURIComponent( proxy.sni || proxy.server, )}${proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''}`; } query += `${ssTransport}${ssAlpn}${ssFp}${ssSecurity}${ssSid}${ssPbk}${ssSpx}${ssMode}${ssExtra}#${encodeURIComponent( proxy.name, )}`; result += query.replace(/^&/, '?'); break; case 'ssr': result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${ proxy.cipher }:${proxy.obfs}:${Base64.encode(proxy.password)}/`; result += `?remarks=${Base64.encode(proxy.name)}${ proxy['obfs-param'] ? '&obfsparam=' + Base64.encode(proxy['obfs-param']) : '' }${ proxy['protocol-param'] ? '&protocolparam=' + Base64.encode(proxy['protocol-param']) : '' }`; result = 'ssr://' + Base64.encode(result); break; case 'vmess': // V2RayN URI format let type = ''; let net = proxy.network || 'tcp'; if (proxy.network === 'http') { net = 'tcp'; type = 'http'; } else if ( proxy.network === 'ws' && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { net = 'httpupgrade'; } result = { v: '2', ps: proxy.name, add: proxy.server, port: `${proxy.port}`, id: proxy.uuid, aid: `${proxy.alterId || 0}`, scy: proxy.cipher, net, type, tls: proxy.tls ? 'tls' : '', alpn: Array.isArray(proxy.alpn) ? proxy.alpn.join(',') : proxy.alpn, fp: proxy['client-fingerprint'], }; if (proxy.tls && proxy.sni) { result.sni = proxy.sni; } // obfs if (proxy.network) { let vmessTransportPath = proxy[`${proxy.network}-opts`]?.path; let vmessTransportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; if (['grpc'].includes(proxy.network)) { result.path = proxy[`${proxy.network}-opts`]?.[ 'grpc-service-name' ]; // https://github.com/XTLS/Xray-core/issues/91 result.type = proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun'; result.host = proxy[`${proxy.network}-opts`]?.['_grpc-authority']; } else if (['kcp', 'quic'].includes(proxy.network)) { // https://github.com/XTLS/Xray-core/issues/91 result.type = proxy[`${proxy.network}-opts`]?.[ `_${proxy.network}-type` ] || 'none'; result.host = proxy[`${proxy.network}-opts`]?.[ `_${proxy.network}-host` ]; result.path = proxy[`${proxy.network}-opts`]?.[ `_${proxy.network}-path` ]; } else { if (vmessTransportPath) { result.path = Array.isArray(vmessTransportPath) ? vmessTransportPath[0] : vmessTransportPath; } if (vmessTransportHost) { result.host = Array.isArray(vmessTransportHost) ? vmessTransportHost[0] : vmessTransportHost; } } } result = 'vmess://' + Base64.encode(JSON.stringify(result)); break; case 'vless': result = vless(proxy); break; case 'trojan': let trojanTransport = ''; if (proxy.network) { let trojanType = proxy.network; if ( proxy.network === 'ws' && proxy['ws-opts']?.['v2ray-http-upgrade'] ) { trojanType = 'httpupgrade'; } trojanTransport = `&type=${encodeURIComponent(trojanType)}`; if (['grpc'].includes(proxy.network)) { let trojanTransportServiceName = proxy[`${proxy.network}-opts`]?.[ `${proxy.network}-service-name` ]; let trojanTransportAuthority = proxy[`${proxy.network}-opts`]?.['_grpc-authority']; if (trojanTransportServiceName) { trojanTransport += `&serviceName=${encodeURIComponent( trojanTransportServiceName, )}`; } if (trojanTransportAuthority) { trojanTransport += `&authority=${encodeURIComponent( trojanTransportAuthority, )}`; } trojanTransport += `&mode=${encodeURIComponent( proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun', )}`; } let trojanTransportPath = proxy[`${proxy.network}-opts`]?.path; let trojanTransportHost = proxy[`${proxy.network}-opts`]?.headers?.Host; if (trojanTransportPath) { trojanTransport += `&path=${encodeURIComponent( Array.isArray(trojanTransportPath) ? trojanTransportPath[0] : trojanTransportPath, )}`; } if (trojanTransportHost) { trojanTransport += `&host=${encodeURIComponent( Array.isArray(trojanTransportHost) ? trojanTransportHost[0] : trojanTransportHost, )}`; } } let trojanFp = ''; if (proxy['client-fingerprint']) { trojanFp = `&fp=${encodeURIComponent( proxy['client-fingerprint'], )}`; } let trojanAlpn = ''; if (proxy.alpn) { trojanAlpn = `&alpn=${encodeURIComponent( Array.isArray(proxy.alpn) ? proxy.alpn : proxy.alpn.join(','), )}`; } const trojanIsReality = proxy['reality-opts']; let trojanSid = ''; let trojanPbk = ''; let trojanSpx = ''; let trojanSecurity = ''; let trojanMode = ''; let trojanExtra = ''; if (trojanIsReality) { trojanSecurity = `&security=reality`; const publicKey = proxy['reality-opts']?.['public-key']; if (publicKey) { trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`; } const shortId = proxy['reality-opts']?.['short-id']; if (shortId) { trojanSid = `&sid=${encodeURIComponent(shortId)}`; } const spiderX = proxy['reality-opts']?.['_spider-x']; if (spiderX) { trojanSpx = `&spx=${encodeURIComponent(spiderX)}`; } if (proxy._extra) { trojanExtra = `&extra=${encodeURIComponent( proxy._extra, )}`; } if (proxy._mode) { trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`; } } result = `trojan://${proxy.password}@${proxy.server}:${ proxy.port }?sni=${encodeURIComponent(proxy.sni || proxy.server)}${ proxy['skip-cert-verify'] ? '&allowInsecure=1' : '' }${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent( proxy.name, )}`; break; case 'hysteria2': let hysteria2params = []; if (proxy['hop-interval']) { hysteria2params.push( `hop-interval=${proxy['hop-interval']}`, ); } if (proxy['keepalive']) { hysteria2params.push(`keepalive=${proxy['keepalive']}`); } if (proxy['skip-cert-verify']) { hysteria2params.push(`insecure=1`); } if (proxy.obfs) { hysteria2params.push( `obfs=${encodeURIComponent(proxy.obfs)}`, ); if (proxy['obfs-password']) { hysteria2params.push( `obfs-password=${encodeURIComponent( proxy['obfs-password'], )}`, ); } } if (proxy.sni) { hysteria2params.push( `sni=${encodeURIComponent(proxy.sni)}`, ); } if (proxy.ports) { hysteria2params.push(`mport=${proxy.ports}`); } if (proxy['tls-fingerprint']) { hysteria2params.push( `pinSHA256=${encodeURIComponent( proxy['tls-fingerprint'], )}`, ); } if (proxy.tfo) { hysteria2params.push(`fastopen=1`); } result = `hysteria2://${encodeURIComponent(proxy.password)}@${ proxy.server }:${proxy.port}?${hysteria2params.join( '&', )}#${encodeURIComponent(proxy.name)}`; break; case 'hysteria': let hysteriaParams = []; Object.keys(proxy).forEach((key) => { if (!['name', 'type', 'server', 'port'].includes(key)) { const i = key.replace(/-/, '_'); if (['alpn'].includes(key)) { if (proxy[key]) { hysteriaParams.push( `${i}=${encodeURIComponent( Array.isArray(proxy[key]) ? proxy[key][0] : proxy[key], )}`, ); } } else if (['skip-cert-verify'].includes(key)) { if (proxy[key]) { hysteriaParams.push(`insecure=1`); } } else if (['tfo', 'fast-open'].includes(key)) { if ( proxy[key] && !hysteriaParams.includes('fastopen=1') ) { hysteriaParams.push(`fastopen=1`); } } else if (['ports'].includes(key)) { hysteriaParams.push(`mport=${proxy[key]}`); } else if (['auth-str'].includes(key)) { hysteriaParams.push(`auth=${proxy[key]}`); } else if (['up'].includes(key)) { hysteriaParams.push(`upmbps=${proxy[key]}`); } else if (['down'].includes(key)) { hysteriaParams.push(`downmbps=${proxy[key]}`); } else if (['_obfs'].includes(key)) { hysteriaParams.push(`obfs=${proxy[key]}`); } else if (['obfs'].includes(key)) { hysteriaParams.push(`obfsParam=${proxy[key]}`); } else if (['sni'].includes(key)) { hysteriaParams.push(`peer=${proxy[key]}`); } else if (proxy[key] && !/^_/i.test(key)) { hysteriaParams.push( `${i}=${encodeURIComponent(proxy[key])}`, ); } } }); result = `hysteria://${proxy.server}:${ proxy.port }?${hysteriaParams.join('&')}#${encodeURIComponent( proxy.name, )}`; break; case 'tuic': if (!proxy.token || proxy.token.length === 0) { let tuicParams = []; Object.keys(proxy).forEach((key) => { if ( ![ 'name', 'type', 'uuid', 'password', 'server', 'port', 'tls', ].includes(key) ) { const i = key.replace(/-/, '_'); if (['alpn'].includes(key)) { if (proxy[key]) { tuicParams.push( `${i}=${encodeURIComponent( Array.isArray(proxy[key]) ? proxy[key][0] : proxy[key], )}`, ); } } else if (['skip-cert-verify'].includes(key)) { if (proxy[key]) { tuicParams.push(`allow_insecure=1`); } } else if (['tfo', 'fast-open'].includes(key)) { if ( proxy[key] && !tuicParams.includes('fast_open=1') ) { tuicParams.push(`fast_open=1`); } } else if ( ['disable-sni', 'reduce-rtt'].includes(key) && proxy[key] ) { tuicParams.push(`${i.replace(/-/g, '_')}=1`); } else if ( ['congestion-controller'].includes(key) ) { tuicParams.push( `congestion_control=${proxy[key]}`, ); } else if (proxy[key] && !/^_/i.test(key)) { tuicParams.push( `${i.replace( /-/g, '_', )}=${encodeURIComponent(proxy[key])}`, ); } } }); result = `tuic://${encodeURIComponent( proxy.uuid, )}:${encodeURIComponent(proxy.password)}@${proxy.server}:${ proxy.port }?${tuicParams.join('&')}#${encodeURIComponent( proxy.name, )}`; } break; case 'anytls': result = vless({ ...proxy, uuid: proxy.password, network: proxy.network || 'tcp', }).replace('vless', 'anytls'); // 偷个懒 let anytlsParams = []; Object.keys(proxy).forEach((key) => { if ( ![ 'name', 'type', 'password', 'server', 'port', 'tls', ].includes(key) ) { const i = key.replace(/-/, '_'); if (['alpn'].includes(key)) { if (proxy[key]) { anytlsParams.push( `${i}=${encodeURIComponent( Array.isArray(proxy[key]) ? proxy[key][0] : proxy[key], )}`, ); } } else if (['skip-cert-verify'].includes(key)) { if (proxy[key]) { anytlsParams.push(`insecure=1`); } } else if (['udp'].includes(key)) { if (proxy[key]) { anytlsParams.push(`udp=1`); } } else if ( proxy[key] && !/^_|client-fingerprint/i.test(key) && ['number', 'string', 'boolean'].includes( typeof proxy[key], ) ) { anytlsParams.push( `${i.replace(/-/g, '_')}=${encodeURIComponent( proxy[key], )}`, ); } } }); // Parse existing query parameters from result const urlParts = result.split('?'); let baseUrl = urlParts[0]; let existingParams = {}; if (urlParts.length > 1) { const queryString = urlParts[1].split('#')[0]; // Remove fragment if exists const pairs = queryString.split('&'); pairs.forEach((pair) => { const [key, value] = pair.split('='); if (key) { existingParams[key] = value; } }); } // Merge anytlsParams with existing parameters anytlsParams.forEach((param) => { const [key, value] = param.split('='); if (key) { existingParams[key] = value; } }); // Reconstruct query string const newParams = Object.keys(existingParams) .map((key) => `${key}=${existingParams[key]}`) .join('&'); // Get fragment part if exists const fragmentMatch = result.match(/#(.*)$/); const fragment = fragmentMatch ? `#${fragmentMatch[1]}` : ''; result = `${baseUrl}?${newParams}${fragment}`; // result = `anytls://${encodeURIComponent(proxy.password)}@${ // proxy.server // }:${proxy.port}/?${anytlsParams.join('&')}#${encodeURIComponent( // proxy.name, // )}`; break; case 'wireguard': let wireguardParams = []; Object.keys(proxy).forEach((key) => { if ( ![ 'name', 'type', 'server', 'port', 'ip', 'ipv6', 'private-key', ].includes(key) ) { if (['public-key'].includes(key)) { wireguardParams.push(`publickey=${proxy[key]}`); } else if (['udp'].includes(key)) { if (proxy[key]) { wireguardParams.push(`${key}=1`); } } else if (proxy[key] && !/^_/i.test(key)) { wireguardParams.push( `${key}=${encodeURIComponent(proxy[key])}`, ); } } }); if (proxy.ip && proxy.ipv6) { wireguardParams.push( `address=${proxy.ip}/32,${proxy.ipv6}/128`, ); } else if (proxy.ip) { wireguardParams.push(`address=${proxy.ip}/32`); } else if (proxy.ipv6) { wireguardParams.push(`address=${proxy.ipv6}/128`); } result = `wireguard://${encodeURIComponent( proxy['private-key'], )}@${proxy.server}:${proxy.port}/?${wireguardParams.join( '&', )}#${encodeURIComponent(proxy.name)}`; break; } return result; }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/producers/utils.js ================================================ import _ from 'lodash'; export class Result { constructor(proxy) { this.proxy = proxy; this.output = []; } append(data) { if (typeof data === 'undefined') { throw new Error('required field is missing'); } this.output.push(data); } appendIfPresent(data, attr) { if (isPresent(this.proxy, attr)) { this.append(data); } } toString() { return this.output.join(''); } } export function isPresent(obj, attr) { const data = _.get(obj, attr); return typeof data !== 'undefined' && data !== null; } ================================================ FILE: backend/src/core/proxy-utils/producers/v2ray.js ================================================ /* eslint-disable no-case-declarations */ import { Base64 } from 'js-base64'; import URI_Producer from './uri'; import $ from '@/core/app'; const URI = URI_Producer(); export default function V2Ray_Producer() { const type = 'ALL'; const produce = (proxies) => { let result = []; proxies.map((proxy) => { try { result.push(URI.produce(proxy)); } catch (err) { $.error( `Cannot produce proxy: ${JSON.stringify( proxy, null, 2, )}\nReason: ${err}`, ); } }); return Base64.encode(result.join('\n')); }; return { type, produce }; } ================================================ FILE: backend/src/core/proxy-utils/validators/index.js ================================================ ================================================ FILE: backend/src/core/rule-utils/index.js ================================================ import RULE_PREPROCESSORS from './preprocessors'; import RULE_PRODUCERS from './producers'; import RULE_PARSERS from './parsers'; import $ from '@/core/app'; export const RuleUtils = (function () { function preprocess(raw) { for (const processor of RULE_PREPROCESSORS) { try { if (processor.test(raw)) { $.info(`Pre-processor [${processor.name}] activated`); return processor.parse(raw); } } catch (e) { $.error(`Parser [${processor.name}] failed\n Reason: ${e}`); } } return raw; } function parse(raw) { raw = preprocess(raw); for (const parser of RULE_PARSERS) { let matched; try { matched = parser.test(raw); } catch (err) { matched = false; } if (matched) { $.info(`Rule parser [${parser.name}] is activated!`); return parser.parse(raw); } } } function produce(rules, targetPlatform) { const producer = RULE_PRODUCERS[targetPlatform]; if (!producer) { throw new Error( `Target platform: ${targetPlatform} is not supported!`, ); } if ( typeof producer.type === 'undefined' || producer.type === 'SINGLE' ) { return rules .map((rule) => { try { return producer.func(rule); } catch (err) { console.log( `ERROR: cannot produce rule: ${JSON.stringify( rule, )}\nReason: ${err}`, ); return ''; } }) .filter((line) => line.length > 0) .join('\n'); } else if (producer.type === 'ALL') { return producer.func(rules); } } return { parse, produce }; })(); ================================================ FILE: backend/src/core/rule-utils/parsers.js ================================================ const RULE_TYPES_MAPPING = [ [/^(DOMAIN|host|HOST)$/, 'DOMAIN'], [/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'], [/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'], [/^USER-AGENT$/i, 'USER-AGENT'], [/^PROCESS-NAME$/, 'PROCESS-NAME'], [/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'], [/^SRC-IP(-CIDR)?$/, 'SRC-IP'], [/^(IN|SRC)-PORT$/, 'IN-PORT'], [/^PROTOCOL$/, 'PROTOCOL'], [/^IP-CIDR$/i, 'IP-CIDR'], [/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'], [/^GEOIP$/i, 'GEOIP'], [/^GEOSITE$/i, 'GEOSITE'], ]; function AllRuleParser() { const name = 'Universal Rule Parser'; const test = () => true; const parse = (raw) => { const lines = raw.split('\n'); const result = []; for (let line of lines) { line = line.trim(); // skip empty line if (line.length === 0) continue; // skip comments if (/\s*#/.test(line)) continue; try { const params = line.split(',').map((w) => w.trim()); let rawType = params[0]; let matched = false; for (const item of RULE_TYPES_MAPPING) { const regex = item[0]; if (regex.test(rawType)) { matched = true; const rule = { type: item[1], content: params[1], }; if ( ['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type) ) { rule.options = params.slice(2); } result.push(rule); } } if (!matched) throw new Error('Invalid rule type: ' + rawType); } catch (e) { console.log(`Failed to parse line: ${line}\n Reason: ${e}`); } } return result; }; return { name, test, parse }; } export default [AllRuleParser()]; ================================================ FILE: backend/src/core/rule-utils/preprocessors.js ================================================ function HTML() { const name = 'HTML'; const test = (raw) => /^/.test(raw); // simply discard HTML const parse = () => ''; return { name, test, parse }; } function ClashProvider() { const name = 'Clash Provider'; const test = (raw) => /^payload:/gm.exec(raw).index >= 0; const parse = (raw) => { return raw.replace('payload:', '').replace(/^\s*-\s*/gm, ''); }; return { name, test, parse }; } export default [HTML(), ClashProvider()]; ================================================ FILE: backend/src/core/rule-utils/producers.js ================================================ import YAML from '@/utils/yaml'; function QXFilter() { const type = 'SINGLE'; const func = (rule) => { // skip unsupported rules const UNSUPPORTED = [ 'URL-REGEX', 'DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL', 'GEOSITE', 'GEOIP', ]; if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; const TRANSFORM = { 'DOMAIN-KEYWORD': 'HOST-KEYWORD', 'DOMAIN-SUFFIX': 'HOST-SUFFIX', DOMAIN: 'HOST', 'IP-CIDR6': 'IP6-CIDR', }; // QX does not support the no-resolve option return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`; }; return { type, func }; } function SurgeRuleSet() { const type = 'SINGLE'; const func = (rule) => { const UNSUPPORTED = ['GEOSITE', 'GEOIP']; if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; let output = `${rule.type},${rule.content}`; if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) { output += rule.options?.length > 0 ? `,${rule.options.join(',')}` : ''; } return output; }; return { type, func }; } function LoonRules() { const type = 'SINGLE'; const func = (rule) => { // skip unsupported rules const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP']; if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) { // Loon only supports the no-resolve option rule.options = rule.options.filter((option) => ['no-resolve'].includes(option), ); } return SurgeRuleSet().func(rule); }; return { type, func }; } function ClashRuleProvider() { const type = 'ALL'; const func = (rules) => { const TRANSFORM = { 'DEST-PORT': 'DST-PORT', 'SRC-IP': 'SRC-IP-CIDR', 'IN-PORT': 'SRC-PORT', }; const conf = { payload: rules.map((rule) => { let output = `${TRANSFORM[rule.type] || rule.type},${ rule.content }`; if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) { if (rule.options) { // Clash only supports the no-resolve option rule.options = rule.options.filter((option) => ['no-resolve'].includes(option), ); } output += rule.options?.length > 0 ? `,${rule.options.join(',')}` : ''; } return output; }), }; return YAML.dump(conf); }; return { type, func }; } export default { QX: QXFilter(), Surge: SurgeRuleSet(), Loon: LoonRules(), Clash: ClashRuleProvider(), }; ================================================ FILE: backend/src/main.js ================================================ /** * ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗ * ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ * ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗ * ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝ * ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * Advanced Subscription Manager for QX, Loon, Surge and Clash. * @author: Peng-YM * @github: https://github.com/sub-store-org/Sub-Store * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 */ import { version } from '../package.json'; import $ from '@/core/app'; console.log( ` ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ Sub-Store -- v${version} ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ `, ); import migrate from '@/utils/migration'; import serve from '@/restful'; migrate(); serve(); ================================================ FILE: backend/src/products/cron-sync-artifacts.js ================================================ import { version } from '../../package.json'; import { SETTINGS_KEY, ARTIFACTS_KEY, SUBS_KEY, COLLECTIONS_KEY, } from '@/constants'; import $ from '@/core/app'; import { produceArtifact } from '@/restful/sync'; import { syncToGist } from '@/restful/artifacts'; import { findByName } from '@/utils/database'; !(async function () { let arg; if (typeof $argument != 'undefined') { arg = Object.fromEntries( // eslint-disable-next-line no-undef $argument.split('&').map((item) => item.split('=')), ); } else { arg = {}; } let sub_names = (arg?.subscription ?? arg?.sub ?? '') .split(/,|,/g) .map((i) => i.trim()) .filter((i) => i.length > 0) .map((i) => decodeURIComponent(i)); let col_names = (arg?.collection ?? arg?.col ?? '') .split(/,|,/g) .map((i) => i.trim()) .filter((i) => i.length > 0) .map((i) => decodeURIComponent(i)); if (sub_names.length > 0 || col_names.length > 0) { if (sub_names.length > 0) await produceArtifacts(sub_names, 'subscription'); if (col_names.length > 0) await produceArtifacts(col_names, 'collection'); } else { const settings = $.read(SETTINGS_KEY); // if GitHub token is not configured if (!settings.githubUser || !settings.gistToken) return; const artifacts = $.read(ARTIFACTS_KEY); if (!artifacts || artifacts.length === 0) return; const shouldSync = artifacts.some((artifact) => artifact.sync); if (shouldSync) await doSync(); } })().finally(() => $.done()); async function produceArtifacts(names, type) { try { if (names.length > 0) { $.info(`produceArtifacts ${type} 开始: ${names.join(', ')}`); await Promise.all( names.map(async (name) => { try { await produceArtifact({ type, name, }); } catch (e) { $.error(`${type} ${name} error: ${e.message ?? e}`); } }), ); $.info(`produceArtifacts ${type} 完成: ${names.join(', ')}`); } } catch (e) { $.error(`produceArtifacts error: ${e.message ?? e}`); } } async function doSync() { console.log( ` ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ Sub-Store Sync -- v${version} ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ `, ); $.info('开始同步所有远程配置...'); const allArtifacts = $.read(ARTIFACTS_KEY); const files = {}; try { const valid = []; const invalid = []; const allSubs = $.read(SUBS_KEY); const allCols = $.read(COLLECTIONS_KEY); const subNames = []; let enabledCount = 0; allArtifacts.map((artifact) => { if (artifact.sync && artifact.source) { enabledCount++; if (artifact.type === 'subscription') { const subName = artifact.source; const sub = findByName(allSubs, subName); if (sub && sub.url && !subNames.includes(subName)) { subNames.push(subName); } } else if (artifact.type === 'collection') { const collection = findByName(allCols, artifact.source); if (collection && Array.isArray(collection.subscriptions)) { collection.subscriptions.map((subName) => { const sub = findByName(allSubs, subName); if (sub && sub.url && !subNames.includes(subName)) { subNames.push(subName); } }); } } } }); if (enabledCount === 0) { $.info( `需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`, ); return; } if (subNames.length > 0) { await Promise.all( subNames.map(async (subName) => { try { await produceArtifact({ type: 'subscription', name: subName, awaitCustomCache: true, }); } catch (e) { // $.error(`${e.message ?? e}`); } }), ); } await Promise.all( allArtifacts.map(async (artifact) => { try { if (artifact.sync && artifact.source) { $.info(`正在同步云配置:${artifact.name}...`); const useMihomoExternal = artifact.platform === 'SurgeMac'; if (useMihomoExternal) { $.info( `手动指定了 target 为 SurgeMac, 将使用 Mihomo External`, ); } const output = await produceArtifact({ type: artifact.type, name: artifact.source, platform: artifact.platform, produceOpts: { 'include-unsupported-proxy': artifact.includeUnsupportedProxy, useMihomoExternal, }, }); // if (!output || output.length === 0) // throw new Error('该配置的结果为空 不进行上传'); files[encodeURIComponent(artifact.name)] = { content: output, }; valid.push(artifact.name); } } catch (e) { $.error( `生成同步配置 ${artifact.name} 发生错误: ${ e.message ?? e }`, ); invalid.push(artifact.name); } }), ); $.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`); $.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`); if (valid.length === 0) { throw new Error( `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`, ); } const resp = await syncToGist(files); const body = JSON.parse(resp.body); delete body.history; delete body.forks; delete body.owner; Object.values(body.files).forEach((file) => { delete file.content; }); $.info('上传配置响应:'); $.info(JSON.stringify(body, null, 2)); for (const artifact of allArtifacts) { if ( artifact.sync && artifact.source && valid.includes(artifact.name) ) { artifact.updated = new Date().getTime(); // extract real url from gist let files = body.files; let isGitLab; if (Array.isArray(files)) { isGitLab = true; files = Object.fromEntries( files.map((item) => [item.path, item]), ); } const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url; const new_url = isGitLab ? raw_url : raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); $.info( `上传配置完成\n文件列表: ${Object.keys(files).join( ', ', )}\n当前文件: ${encodeURIComponent( artifact.name, )}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`, ); artifact.url = new_url; } } $.write(allArtifacts, ARTIFACTS_KEY); $.info('上传配置成功'); if (invalid.length > 0) { $.notify( '🌍 Sub-Store', `同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`, ); } else { $.notify('🌍 Sub-Store', '同步配置完成'); } } catch (e) { $.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`); $.error(`无法同步配置到 Gist,原因:${e}`); } } ================================================ FILE: backend/src/products/resource-parser.loon.js ================================================ /* eslint-disable no-undef */ import { ProxyUtils } from '@/core/proxy-utils'; import { RuleUtils } from '@/core/rule-utils'; import { version } from '../../package.json'; import download from '@/utils/download'; let result = ''; let resource = typeof $resource !== 'undefined' ? $resource : ''; let resourceType = typeof $resourceType !== 'undefined' ? $resourceType : ''; let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : ''; !(async () => { console.log( ` ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ Sub-Store -- v${version} Loon -- ${$loon} ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ `, ); const build = $loon.match(/\((\d+)\)$/)?.[1]; let arg; if (typeof $argument != 'undefined') { arg = Object.fromEntries( $argument.split('&').map((item) => item.split('=')), ); } else { arg = {}; } console.log(`arg: ${JSON.stringify(arg)}`); const RESOURCE_TYPE = { PROXY: 1, RULE: 2, }; if (!arg.resourceUrlOnly) { result = resource; } if (resourceType === RESOURCE_TYPE.PROXY) { if (!arg.resourceUrlOnly) { try { let proxies = ProxyUtils.parse(resource); result = ProxyUtils.produce(proxies, 'Loon', undefined, { 'include-unsupported-proxy': arg?.includeUnsupportedProxy || build >= 842, }); } catch (e) { console.log('解析器: 使用 resource 出现错误'); console.log(e.message ?? e); } } if ((!result || /^\s*$/.test(result)) && resourceUrl) { console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`); try { let raw = await download( resourceUrl, arg?.ua, arg?.timeout, undefined, undefined, undefined, undefined, true, ); let proxies = ProxyUtils.parse(raw); result = ProxyUtils.produce(proxies, 'Loon', undefined, { 'include-unsupported-proxy': arg?.includeUnsupportedProxy || build >= 842, }); } catch (e) { console.log(e.message ?? e); } } } else if (resourceType === RESOURCE_TYPE.RULE) { if (!arg.resourceUrlOnly) { try { const rules = RuleUtils.parse(resource); result = RuleUtils.produce(rules, 'Loon'); } catch (e) { console.log(e.message ?? e); } } if ((!result || /^\s*$/.test(result)) && resourceUrl) { console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`); try { let raw = await download(resourceUrl, arg?.ua, arg?.timeout); let rules = RuleUtils.parse(raw); result = RuleUtils.produce(rules, 'Loon'); } catch (e) { console.log(e.message ?? e); } } } })() .catch(async (e) => { console.log('解析器: 出现错误'); console.log(e.message ?? e); }) .finally(() => { $done(result || ''); }); ================================================ FILE: backend/src/products/sub-store-0.js ================================================ /** * 路由拆分 - 本文件只包含不涉及到解析器的 RESTFul API */ import { version } from '../../package.json'; console.log( ` ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ Sub-Store -- v${version} ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ `, ); import migrate from '@/utils/migration'; import express from '@/vendor/express'; import $ from '@/core/app'; import registerCollectionRoutes from '@/restful/collections'; import registerSubscriptionRoutes from '@/restful/subscriptions'; import registerArtifactRoutes from '@/restful/artifacts'; import registerSettingRoutes from '@/restful/settings'; import registerMiscRoutes from '@/restful/miscs'; import registerSortRoutes from '@/restful/sort'; import registerFileRoutes from '@/restful/file'; import registerTokenRoutes from '@/restful/token'; import registerModuleRoutes from '@/restful/module'; migrate(); serve(); function serve() { const $app = express({ substore: $ }); // register routes registerCollectionRoutes($app); registerSubscriptionRoutes($app); registerTokenRoutes($app); registerFileRoutes($app); registerModuleRoutes($app); registerArtifactRoutes($app); registerSettingRoutes($app); registerSortRoutes($app); registerMiscRoutes($app); $app.start(); } ================================================ FILE: backend/src/products/sub-store-1.js ================================================ /** * 路由拆分 - 本文件仅包含使用到解析器的 RESTFul API */ import { version } from '../../package.json'; import migrate from '@/utils/migration'; import express from '@/vendor/express'; import $ from '@/core/app'; import registerDownloadRoutes from '@/restful/download'; import registerPreviewRoutes from '@/restful/preview'; import registerSyncRoutes from '@/restful/sync'; import registerNodeInfoRoutes from '@/restful/node-info'; console.log( ` ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ Sub-Store -- v${version} ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ `, ); migrate(); serve(); function serve() { const $app = express({ substore: $ }); // register routes registerDownloadRoutes($app); registerPreviewRoutes($app); registerSyncRoutes($app); registerNodeInfoRoutes($app); $app.options('/', (req, res) => { res.status(200).end(); }); $app.start(); } ================================================ FILE: backend/src/restful/artifacts.js ================================================ import $ from '@/core/app'; import { ARTIFACT_REPOSITORY_KEY, ARTIFACTS_KEY, SETTINGS_KEY, } from '@/constants'; import { deleteByName, findByName, updateByName } from '@/utils/database'; import { failed, success } from '@/restful/response'; import { InternalServerError, RequestInvalidError, ResourceNotFoundError, } from '@/restful/errors'; import Gist from '@/utils/gist'; export default function register($app) { // Initialization if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY); // RESTful APIs $app.get('/api/artifacts/restore', restoreArtifacts); $app.route('/api/artifacts') .get(getAllArtifacts) .post(createArtifact) .put(replaceArtifact); $app.route('/api/artifact/:name') .get(getArtifact) .patch(updateArtifact) .delete(deleteArtifact); } async function restoreArtifacts(_, res) { $.info('开始恢复远程配置...'); try { const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); if (!gistToken) { return Promise.reject('未设置 GitHub Token!'); } const manager = new Gist({ token: gistToken, key: ARTIFACT_REPOSITORY_KEY, syncPlatform, }); try { const gist = await manager.locate(); if (!gist?.files) { throw new Error(`找不到 Sub-Store Gist 文件列表`); } const allArtifacts = $.read(ARTIFACTS_KEY); const failed = []; Object.keys(gist.files).map((key) => { const filename = gist.files[key]?.filename; if (filename) { if (encodeURIComponent(filename) !== filename) { $.error(`文件名 ${filename} 未编码 不保存`); failed.push(filename); } else { const artifact = findByName(allArtifacts, filename); if (artifact) { updateByName(allArtifacts, filename, { ...artifact, url: gist.files[key]?.raw_url.replace( /\/raw\/[^/]*\/(.*)/, '/raw/$1', ), }); } else { allArtifacts.push({ name: `${filename}`, url: gist.files[key]?.raw_url.replace( /\/raw\/[^/]*\/(.*)/, '/raw/$1', ), }); } } } }); $.write(allArtifacts, ARTIFACTS_KEY); } catch (err) { $.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`); throw err; } success(res); } catch (e) { $.error(`恢复远程配置失败,原因:${e.message ?? e}`); failed( res, new InternalServerError( `FAILED_TO_RESTORE_ARTIFACTS`, `Failed to restore artifacts`, `Reason: ${e.message ?? e}`, ), ); } } function getAllArtifacts(req, res) { const allArtifacts = $.read(ARTIFACTS_KEY); success(res, allArtifacts); } function replaceArtifact(req, res) { const allArtifacts = req.body; $.write(allArtifacts, ARTIFACTS_KEY); success(res); } async function getArtifact(req, res) { let { name } = req.params; const allArtifacts = $.read(ARTIFACTS_KEY); const artifact = findByName(allArtifacts, name); if (artifact) { success(res, artifact); } else { failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Artifact ${name} does not exist!`, ), 404, ); } } function createArtifact(req, res) { const artifact = req.body; if (!validateArtifactName(artifact.name)) { failed( res, new RequestInvalidError( 'INVALID_ARTIFACT_NAME', `Artifact name ${artifact.name} is invalid.`, ), ); return; } $.info(`正在创建远程配置:${artifact.name}`); const allArtifacts = $.read(ARTIFACTS_KEY); if (findByName(allArtifacts, artifact.name)) { failed( res, new RequestInvalidError( 'DUPLICATE_KEY', `Artifact ${artifact.name} already exists.`, ), ); } else { allArtifacts.push(artifact); $.write(allArtifacts, ARTIFACTS_KEY); success(res, artifact, 201); } } function updateArtifact(req, res) { let artifact = req.body; const allArtifacts = $.read(ARTIFACTS_KEY); let oldName = req.params.name; const oldArtifact = findByName(allArtifacts, oldName); if (oldArtifact) { if (!artifact.name) artifact.name = oldArtifact.name; $.info(`正在更新远程配置:${oldArtifact.name}`); const newArtifact = { ...oldArtifact, ...artifact, }; if (!validateArtifactName(newArtifact.name)) { failed( res, new RequestInvalidError( 'INVALID_ARTIFACT_NAME', `Artifact name ${newArtifact.name} is invalid.`, ), ); return; } updateByName(allArtifacts, oldName, newArtifact); $.write(allArtifacts, ARTIFACTS_KEY); success(res, newArtifact); } else { failed( res, new RequestInvalidError( 'DUPLICATE_KEY', `Artifact ${oldName} already exists.`, ), ); } } async function deleteArtifact(req, res) { let { name } = req.params; $.info(`正在删除远程配置:${name}`); const allArtifacts = $.read(ARTIFACTS_KEY); try { const artifact = findByName(allArtifacts, name); if (!artifact) throw new Error(`远程配置:${name}不存在!`); if (artifact.updated) { // delete gist const files = {}; files[encodeURIComponent(artifact.name)] = { content: '', }; if (encodeURIComponent(artifact.name) !== artifact.name) { files[artifact.name] = { content: '', }; } // 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug try { await syncToGist(files); } catch (i) { $.error(`Function syncToGist: ${name} : ${i}`); } } // delete local cache deleteByName(allArtifacts, name); $.write(allArtifacts, ARTIFACTS_KEY); success(res); } catch (err) { $.error(`无法删除远程配置:${name},原因:${err}`); failed( res, new InternalServerError( `FAILED_TO_DELETE_ARTIFACT`, `Failed to delete artifact ${name}`, `Reason: ${err}`, ), ); } } function validateArtifactName(name) { return /^[a-zA-Z0-9._-]*$/.test(name); } async function syncToGist(files) { const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); if (!gistToken) { return Promise.reject('未设置 GitHub Token!'); } const manager = new Gist({ token: gistToken, key: ARTIFACT_REPOSITORY_KEY, syncPlatform, }); const res = await manager.upload(files); let body = {}; try { body = JSON.parse(res.body); // eslint-disable-next-line no-empty } catch (e) {} const url = body?.html_url ?? body?.web_url; const settings = $.read(SETTINGS_KEY); if (url) { $.log(`同步 Gist 后, 找到 Sub-Store Gist: ${url}`); settings.artifactStore = url; settings.artifactStoreStatus = 'VALID'; } else { $.error(`同步 Gist 后, 找不到 Sub-Store Gist`); settings.artifactStoreStatus = 'NOT FOUND'; } $.write(settings, SETTINGS_KEY); return res; } export { syncToGist }; ================================================ FILE: backend/src/restful/collections.js ================================================ import { deleteByName, findByName, updateByName } from '@/utils/database'; import { COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY } from '@/constants'; import { failed, success } from '@/restful/response'; import $ from '@/core/app'; import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors'; import { formatDateTime } from '@/utils'; export default function register($app) { if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY); $app.route('/api/collection/:name') .get(getCollection) .patch(updateCollection) .delete(deleteCollection); $app.route('/api/collections') .get(getAllCollections) .post(createCollection) .put(replaceCollection); } // collection API function createCollection(req, res) { const collection = req.body; $.info(`正在创建组合订阅:${collection.name}`); if (/\//.test(collection.name)) { failed( res, new RequestInvalidError( 'INVALID_NAME', `Collection ${collection.name} is invalid`, ), ); return; } const allCols = $.read(COLLECTIONS_KEY); if (findByName(allCols, collection.name)) { failed( res, new RequestInvalidError( 'DUPLICATE_KEY', `Collection ${collection.name} already exists.`, ), ); return; } allCols.push(collection); $.write(allCols, COLLECTIONS_KEY); success(res, collection, 201); } function getCollection(req, res) { let { name } = req.params; let { raw } = req.query; const allCols = $.read(COLLECTIONS_KEY); const collection = findByName(allCols, name); if (collection) { if (raw) { res.set('content-type', 'application/json') .set( 'content-disposition', `attachment; filename="${encodeURIComponent( `sub-store_collection_${name}_${formatDateTime( new Date(), )}.json`, )}"`, ) .send(JSON.stringify(collection)); } else { success(res, collection); } } else { failed( res, new ResourceNotFoundError( `SUBSCRIPTION_NOT_FOUND`, `Collection ${name} does not exist`, 404, ), ); } } function updateCollection(req, res) { let { name } = req.params; let collection = req.body; const allCols = $.read(COLLECTIONS_KEY); const oldCol = findByName(allCols, name); if (oldCol) { if (!collection.name) collection.name = oldCol.name; const newCol = { ...oldCol, ...collection, }; $.info(`正在更新组合订阅:${name}...`); if (name !== newCol.name) { // update all artifacts referring this collection const allArtifacts = $.read(ARTIFACTS_KEY) || []; for (const artifact of allArtifacts) { if ( artifact.type === 'collection' && artifact.source === oldCol.name ) { artifact.source = newCol.name; } } // update all files referring this collection const allFiles = $.read(FILES_KEY) || []; for (const file of allFiles) { if ( file.sourceType === 'collection' && file.sourceName === oldCol.name ) { file.sourceName = newCol.name; } } $.write(allArtifacts, ARTIFACTS_KEY); $.write(allFiles, FILES_KEY); } updateByName(allCols, name, newCol); $.write(allCols, COLLECTIONS_KEY); success(res, newCol); } else { failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Collection ${name} does not exist!`, ), 404, ); } } function deleteCollection(req, res) { let { name } = req.params; $.info(`正在删除组合订阅:${name}`); let allCols = $.read(COLLECTIONS_KEY); deleteByName(allCols, name); $.write(allCols, COLLECTIONS_KEY); success(res); } function getAllCollections(req, res) { const allCols = $.read(COLLECTIONS_KEY); success(res, allCols); } function replaceCollection(req, res) { const allCols = req.body; $.write(allCols, COLLECTIONS_KEY); success(res); } ================================================ FILE: backend/src/restful/download.js ================================================ import { getPlatformFromHeaders, shouldIncludeUnsupportedProxy, } from '@/utils/user-agent'; import { ProxyUtils } from '@/core/proxy-utils'; import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants'; import { findByName } from '@/utils/database'; import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow'; import $ from '@/core/app'; import { failed } from '@/restful/response'; import { InternalServerError, ResourceNotFoundError } from '@/restful/errors'; import { produceArtifact } from '@/restful/sync'; // eslint-disable-next-line no-unused-vars import { isIPv4, isIPv6 } from '@/utils'; import { getISO } from '@/utils/geo'; import env from '@/utils/env'; export default function register($app) { $app.get('/share/col/:name/:target', async (req, res) => { const { target } = req.params; if (target) { req.query.target = target; $.info(`使用路由指定目标: ${target}`); } await downloadCollection(req, res); }); $app.get('/share/col/:name', downloadCollection); $app.get('/share/sub/:name/:target', async (req, res) => { const { target } = req.params; if (target) { req.query.target = target; $.info(`使用路由指定目标: ${target}`); } await downloadSubscription(req, res); }); $app.get('/share/sub/:name', downloadSubscription); $app.get('/download/collection/:name/:target', async (req, res) => { const { target } = req.params; if (target) { req.query.target = target; $.info(`使用路由指定目标: ${target}`); } await downloadCollection(req, res); }); $app.get('/download/collection/:name', downloadCollection); $app.get('/download/:name/:target', async (req, res) => { const { target } = req.params; if (target) { req.query.target = target; $.info(`使用路由指定目标: ${target}`); } await downloadSubscription(req, res); }); $app.get('/download/:name', downloadSubscription); $app.get( '/download/collection/:name/api/v1/server/details', async (req, res) => { req.query.platform = 'JSON'; req.query.produceType = 'internal'; req.query.resultFormat = 'nezha'; await downloadCollection(req, res); }, ); $app.get('/download/:name/api/v1/server/details', async (req, res) => { req.query.platform = 'JSON'; req.query.produceType = 'internal'; req.query.resultFormat = 'nezha'; await downloadSubscription(req, res); }); $app.get( '/download/collection/:name/api/v1/monitor/:nezhaIndex', async (req, res) => { req.query.platform = 'JSON'; req.query.produceType = 'internal'; req.query.resultFormat = 'nezha-monitor'; await downloadCollection(req, res); }, ); $app.get('/download/:name/api/v1/monitor/:nezhaIndex', async (req, res) => { req.query.platform = 'JSON'; req.query.produceType = 'internal'; req.query.resultFormat = 'nezha-monitor'; await downloadSubscription(req, res); }); } async function downloadSubscription(req, res) { let { name, nezhaIndex } = req.params; const useMihomoExternal = req.query.target === 'SurgeMac'; const platform = req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; const reqUA = req.headers['user-agent'] || req.headers['User-Agent']; $.info( `正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`, ); let { url, ua, content, mergeSources, ignoreFailedRemoteSub, produceType, includeUnsupportedProxy, resultFormat, proxy, noCache, _fakeNode, } = req.query; let $options = { _req: { method: req.method, url: req.url, path: req.path, query: req.query, params: req.params, headers: req.headers, body: req.body, }, }; if (req.query.$options) { let options = {}; try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` options = JSON.parse(decodeURIComponent(req.query.$options)); } catch (e) { for (const pair of req.query.$options.split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; options[key] = value == null || value === '' ? true : decodeURIComponent(value); } } $.info(`传入 $options: ${JSON.stringify(options)}`); Object.assign($options, options); } if (url) { $.info(`指定远程订阅 URL: ${url}`); if (!/^https?:\/\//.test(url)) { content = url; $.info(`URL 不是链接,视为本地订阅`); } } if (content) { $.info(`指定本地订阅: ${content}`); } if (proxy) { $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`); } if (ua) { $.info(`指定远程订阅 User-Agent: ${ua}`); } if (mergeSources) { $.info(`指定合并来源: ${mergeSources}`); } if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`); } if (produceType) { $.info(`指定生产类型: ${produceType}`); } if (includeUnsupportedProxy) { $.info( `包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`, ); } if ( !includeUnsupportedProxy && shouldIncludeUnsupportedProxy(platform, req.headers) ) { includeUnsupportedProxy = true; $.info( `当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`, ); } if (useMihomoExternal) { $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`); } if (noCache) { $.info(`指定不使用缓存: ${noCache}`); } const allSubs = $.read(SUBS_KEY); const fakeSub = { name: 'fakeNodeInfo', source: 'local', content: 'invalid share = ss, 1.0.0.1, 80, encrypt-method=aes-128-gcm, password=password', }; const sub = _fakeNode ? fakeSub : findByName(allSubs, name); if (sub) { try { const passThroughUA = sub.passThroughUA; if (passThroughUA) { $.info( `订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`, ); ua = reqUA; } const opt = { type: 'subscription', name, platform, url, ua, content, mergeSources, ignoreFailedRemoteSub, produceType, produceOpts: { 'include-unsupported-proxy': includeUnsupportedProxy, useMihomoExternal, }, $options, proxy, noCache, }; if (_fakeNode) { $.info(`返回假节点信息`); delete opt.name; opt.subscription = fakeSub; } let output = await produceArtifact(opt); let flowInfo; if ( sub.source !== 'local' || ['localFirst', 'remoteFirst'].includes(sub.mergeSources) ) { try { url = `${url || sub.url}` .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length)?.[0] || ''; let $arguments = {}; const rawArgs = url.split('#'); url = url.split('#')[0]; if (rawArgs.length > 1) { try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` $arguments = JSON.parse( decodeURIComponent(rawArgs[1]), ); } catch (e) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; $arguments[key] = value == null || value === '' ? true : decodeURIComponent(value); } } } if (!$arguments.noFlow && /^https?/.test(url)) { // forward flow headers flowInfo = await getFlowHeaders( $arguments?.insecure ? `${url}#insecure` : url, $arguments.flowUserAgent, undefined, proxy || sub.proxy, $arguments.flowUrl, ); if (flowInfo) { const headers = normalizeFlowHeader(flowInfo, true); if (headers?.['subscription-userinfo']) { res.set( 'subscription-userinfo', headers['subscription-userinfo'], ); } if (headers?.['profile-web-page-url']) { res.set( 'profile-web-page-url', headers['profile-web-page-url'], ); } if (headers?.['plan-name']) { res.set('plan-name', headers['plan-name']); } } } } catch (err) { $.error( `订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify( err, )}`, ); } } if (sub.subUserinfo) { let subUserInfo; if (/^https?:\/\//.test(sub.subUserinfo)) { try { subUserInfo = await getFlowHeaders( undefined, undefined, undefined, proxy || sub.proxy, sub.subUserinfo, ); } catch (e) { $.error( `订阅 ${name} 使用自定义流量链接 ${ sub.subUserinfo } 获取流量信息时发生错误: ${JSON.stringify(e)}`, ); } } else { subUserInfo = sub.subUserinfo; } const headers = normalizeFlowHeader( [subUserInfo, flowInfo].filter((i) => i).join(';'), true, ); if (headers?.['subscription-userinfo']) { res.set( 'subscription-userinfo', headers['subscription-userinfo'], ); } if (headers?.['profile-web-page-url']) { res.set( 'profile-web-page-url', headers['profile-web-page-url'], ); } if (headers?.['plan-name']) { res.set('plan-name', headers['plan-name']); } } if (platform === 'JSON') { if (resultFormat === 'nezha') { output = nezhaTransform(output); } else if (resultFormat === 'nezha-monitor') { nezhaIndex = /^\d+$/.test(nezhaIndex) ? parseInt(nezhaIndex, 10) : output.findIndex((i) => i.name === nezhaIndex); output = await nezhaMonitor( output[nezhaIndex], nezhaIndex, req.query, ); } res.set('Content-Type', 'application/json;charset=utf-8'); } else { res.set('Content-Type', 'text/plain; charset=utf-8'); } if ($options?._res?.headers) { Object.entries($options._res.headers).forEach( ([key, value]) => { if (value == null) { res.removeHeader(key); } else { res.set(key, value); } }, ); } if ($options?._res?.status) { res.status($options._res.status); } res.send(output); } catch (err) { $.notify( `🌍 Sub-Store 下载订阅失败`, `❌ 无法下载订阅:${name}!`, `🤔 原因:${err.message ?? err}`, ); $.error(err.message ?? err); failed( res, new InternalServerError( 'INTERNAL_SERVER_ERROR', `Failed to download subscription: ${name}`, `Reason: ${err.message ?? err}`, ), ); } } else { $.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}!`); failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Subscription ${name} does not exist!`, ), 404, ); } } async function downloadCollection(req, res) { let { name, nezhaIndex } = req.params; const useMihomoExternal = req.query.target === 'SurgeMac'; const platform = req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; const allCols = $.read(COLLECTIONS_KEY); const collection = findByName(allCols, name); const reqUA = req.headers['user-agent'] || req.headers['User-Agent']; $.info( `正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`, ); let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy, resultFormat, proxy, noCache, } = req.query; let $options = { _req: { method: req.method, url: req.url, path: req.path, query: req.query, params: req.params, headers: req.headers, body: req.body, }, }; if (req.query.$options) { let options = {}; try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` options = JSON.parse(decodeURIComponent(req.query.$options)); } catch (e) { for (const pair of req.query.$options.split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; options[key] = value == null || value === '' ? true : decodeURIComponent(value); } } $.info(`传入 $options: ${JSON.stringify(options)}`); Object.assign($options, options); } if (proxy) { $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`); } if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`); } if (produceType) { $.info(`指定生产类型: ${produceType}`); } if (includeUnsupportedProxy) { $.info( `包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`, ); } if ( !includeUnsupportedProxy && shouldIncludeUnsupportedProxy(platform, req.headers) ) { includeUnsupportedProxy = true; $.info( `当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`, ); } if (useMihomoExternal) { $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`); } if (noCache) { $.info(`指定不使用缓存: ${noCache}`); } if (collection) { try { let output = await produceArtifact({ type: 'collection', name, platform, ignoreFailedRemoteSub, produceType, produceOpts: { 'include-unsupported-proxy': includeUnsupportedProxy, useMihomoExternal, }, $options, proxy, noCache, ua: reqUA, }); let subUserInfoOfSub; // forward flow header from the first subscription in this collection const allSubs = $.read(SUBS_KEY); const subnames = collection.subscriptions; if (subnames.length > 0) { const sub = findByName(allSubs, subnames[0]); if ( sub.source !== 'local' || ['localFirst', 'remoteFirst'].includes(sub.mergeSources) ) { try { let url = `${sub.url}` .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length)?.[0] || ''; let $arguments = {}; const rawArgs = url.split('#'); url = url.split('#')[0]; if (rawArgs.length > 1) { try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` $arguments = JSON.parse( decodeURIComponent(rawArgs[1]), ); } catch (e) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; $arguments[key] = value == null || value === '' ? true : decodeURIComponent(value); } } } if (!$arguments.noFlow && /^https?:/.test(url)) { subUserInfoOfSub = await getFlowHeaders( $arguments?.insecure ? `${url}#insecure` : url, $arguments.flowUserAgent, undefined, proxy || sub.proxy || collection.proxy, $arguments.flowUrl, ); } } catch (err) { $.error( `组合订阅 ${name} 中的子订阅 ${ sub.name } 获取流量信息时发生错误: ${err.message ?? err}`, ); } } if (sub.subUserinfo) { let subUserInfo; if (/^https?:\/\//.test(sub.subUserinfo)) { try { subUserInfo = await getFlowHeaders( undefined, undefined, undefined, proxy || sub.proxy, sub.subUserinfo, ); } catch (e) { $.error( `组合订阅 ${name} 使用自定义流量链接 ${ sub.subUserinfo } 获取流量信息时发生错误: ${JSON.stringify(e)}`, ); } } else { subUserInfo = sub.subUserinfo; } subUserInfoOfSub = [subUserInfo, subUserInfoOfSub] .filter((i) => i) .join('; '); } } $.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`); let subUserInfoOfCol; if (/^https?:\/\//.test(collection.subUserinfo)) { try { subUserInfoOfCol = await getFlowHeaders( undefined, undefined, undefined, proxy || collection.proxy, collection.subUserinfo, ); } catch (e) { $.error( `组合订阅 ${name} 使用自定义流量链接 ${ collection.subUserinfo } 获取流量信息时发生错误: ${JSON.stringify(e)}`, ); } } else { subUserInfoOfCol = collection.subUserinfo; } const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub] .filter((i) => i) .join('; '); if (subUserInfo) { const headers = normalizeFlowHeader(subUserInfo, true); if (headers?.['subscription-userinfo']) { res.set( 'subscription-userinfo', headers['subscription-userinfo'], ); } if (headers?.['profile-web-page-url']) { res.set( 'profile-web-page-url', headers['profile-web-page-url'], ); } if (headers?.['plan-name']) { res.set('plan-name', headers['plan-name']); } } if (platform === 'JSON') { if (resultFormat === 'nezha') { output = nezhaTransform(output); } else if (resultFormat === 'nezha-monitor') { nezhaIndex = /^\d+$/.test(nezhaIndex) ? parseInt(nezhaIndex, 10) : output.findIndex((i) => i.name === nezhaIndex); output = await nezhaMonitor( output[nezhaIndex], nezhaIndex, req.query, ); } res.set('Content-Type', 'application/json;charset=utf-8'); } else { res.set('Content-Type', 'text/plain; charset=utf-8'); } if ($options?._res?.headers) { Object.entries($options._res.headers).forEach( ([key, value]) => { if (value == null) { res.removeHeader(key); } else { res.set(key, value); } }, ); } if ($options?._res?.status) { res.status($options._res.status); } res.send(output); } catch (err) { $.notify( `🌍 Sub-Store 下载组合订阅失败`, `❌ 下载组合订阅错误:${name}!`, `🤔 原因:${err}`, ); failed( res, new InternalServerError( 'INTERNAL_SERVER_ERROR', `Failed to download collection: ${name}`, `Reason: ${err.message ?? err}`, ), ); } } else { $.error( `🌍 Sub-Store 下载组合订阅失败`, `❌ 未找到组合订阅:${name}!`, ); failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Collection ${name} does not exist!`, ), 404, ); } } async function nezhaMonitor(proxy, index, query) { const result = { code: 0, message: 'success', result: [], }; try { const { isLoon, isSurge } = $.env; if (!isLoon && !isSurge) throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)'); const node = ProxyUtils.produce([proxy], isLoon ? 'Loon' : 'Surge'); if (!node) throw new Error('当前客户端不兼容此节点'); const monitors = proxy._monitors || [ { name: 'Cloudflare', url: 'http://cp.cloudflare.com/generate_204', method: 'HEAD', number: 3, timeout: 2000, }, { name: 'Google', url: 'http://www.google.com/generate_204', method: 'HEAD', number: 3, timeout: 2000, }, ]; const number = query.number || Math.max(...monitors.map((i) => i.number)) || 3; for (const monitor of monitors) { const interval = 10 * 60 * 1000; const data = { monitor_id: monitors.indexOf(monitor), server_id: index, monitor_name: monitor.name, server_name: proxy.name, created_at: [], avg_delay: [], }; for (let index = 0; index < number; index++) { const startedAt = Date.now(); try { await $.http[(monitor.method || 'HEAD').toLowerCase()]({ timeout: monitor.timeout || 2000, url: monitor.url, 'policy-descriptor': node, node, }); const latency = Date.now() - startedAt; $.info(`${monitor.name} latency: ${latency}`); data.avg_delay.push(latency); } catch (e) { $.error(e); data.avg_delay.push(0); } data.created_at.push( Date.now() - interval * (monitor.number - index - 1), ); } result.result.push(data); } } catch (e) { $.error(e); result.result.push({ monitor_id: 0, server_id: 0, monitor_name: `❌ ${e.message ?? e}`, server_name: proxy.name, created_at: [Date.now()], avg_delay: [0], }); } return JSON.stringify(result, null, 2); } function nezhaTransform(output) { const result = { code: 0, message: 'success', result: [], }; output.map((proxy, index) => { // 如果节点上有数据 就取节点上的数据 let CountryCode = proxy._geo?.countryCode || proxy._geo?.country; // 简单判断下 if (!/^[a-z]{2}$/i.test(CountryCode)) { CountryCode = getISO(proxy.name); } // 简单判断下 if (/^[a-z]{2}$/i.test(CountryCode)) { // 如果节点上有数据 就取节点上的数据 let now = Math.round(new Date().getTime() / 1000); let time = proxy._unavailable ? 0 : now; const uptime = parseInt(proxy._uptime || 0, 10); result.result.push({ id: index, name: proxy.name, tag: `${proxy._tag ?? ''}`, last_active: time, // 暂时不用处理 现在 VPings App 端的接口支持域名查询 // 其他场景使用 自己在 Sub-Store 加一步域名解析 valid_ip: proxy._IP || proxy.server, ipv4: proxy._IPv4 || proxy.server, ipv6: proxy._IPv6 || (isIPv6(proxy.server) ? proxy.server : ''), host: { Platform: 'Sub-Store', PlatformVersion: env.version, CPU: [], MemTotal: 1024, DiskTotal: 1024, SwapTotal: 1024, Arch: '', Virtualization: '', BootTime: now - uptime, CountryCode, // 目前需要 Version: '0.0.1', }, status: { CPU: 0, MemUsed: 0, SwapUsed: 0, DiskUsed: 0, NetInTransfer: 0, NetOutTransfer: 0, NetInSpeed: 0, NetOutSpeed: 0, Uptime: uptime, Load1: 0, Load5: 0, Load15: 0, TcpConnCount: 0, UdpConnCount: 0, ProcessCount: 0, }, }); } }); return JSON.stringify(result, null, 2); } ================================================ FILE: backend/src/restful/errors/index.js ================================================ class BaseError { constructor(code, message, details) { this.code = code; this.message = message; this.details = details; } } export class InternalServerError extends BaseError { constructor(code, message, details) { super(code, message, details); this.type = 'InternalServerError'; } } export class RequestInvalidError extends BaseError { constructor(code, message, details) { super(code, message, details); this.type = 'RequestInvalidError'; } } export class ResourceNotFoundError extends BaseError { constructor(code, message, details) { super(code, message, details); this.type = 'ResourceNotFoundError'; } } export class NetworkError extends BaseError { constructor(code, message, details) { super(code, message, details); this.type = 'NetworkError'; } } ================================================ FILE: backend/src/restful/file.js ================================================ import { deleteByName, findByName, updateByName } from '@/utils/database'; import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow'; import { FILES_KEY, ARTIFACTS_KEY } from '@/constants'; import { failed, success } from '@/restful/response'; import $ from '@/core/app'; import { RequestInvalidError, ResourceNotFoundError, InternalServerError, } from '@/restful/errors'; import { produceArtifact } from '@/restful/sync'; import { formatDateTime } from '@/utils'; export default function register($app) { if (!$.read(FILES_KEY)) $.write([], FILES_KEY); $app.get('/share/file/:name', getFile); $app.route('/api/file/:name') .get(getFile) .patch(updateFile) .delete(deleteFile); $app.route('/api/wholeFile/:name').get(getWholeFile); $app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile); $app.route('/api/wholeFiles').get(getAllWholeFiles); } // file API function createFile(req, res) { const file = req.body; file.name = `${file.name ?? Date.now()}`; $.info(`正在创建文件:${file.name}`); const allFiles = $.read(FILES_KEY); if (findByName(allFiles, file.name)) { return failed( res, new RequestInvalidError( 'DUPLICATE_KEY', req.body.name ? `已存在 name 为 ${file.name} 的文件` : `无法同时创建相同的文件 可稍后重试`, ), ); } allFiles.push(file); $.write(allFiles, FILES_KEY); success(res, file, 201); } async function getFile(req, res, next) { let { name } = req.params; const reqUA = req.headers['user-agent'] || req.headers['User-Agent']; $.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`); let { url, subInfoUrl, subInfoUserAgent, ua, content, mergeSources, ignoreFailedRemoteFile, proxy, noCache, produceType, } = req.query; let $options = { _req: { method: req.method, url: req.url, path: req.path, query: req.query, params: req.params, headers: req.headers, body: req.body, }, }; if (req.query.$options) { let options = {}; try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` options = JSON.parse(decodeURIComponent(req.query.$options)); } catch (e) { for (const pair of req.query.$options.split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; options[key] = value == null || value === '' ? true : decodeURIComponent(value); } } $.info(`传入 $options: ${JSON.stringify(options)}`); Object.assign($options, options); } if (url) { $.info(`指定远程文件 URL: ${url}`); } if (proxy) { $.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`); } if (ua) { $.info(`指定远程文件 User-Agent: ${ua}`); } if (subInfoUrl) { $.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`); } if (subInfoUserAgent) { $.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`); } if (content) { $.info(`指定本地文件: ${content}`); } if (mergeSources) { $.info(`指定合并来源: ${mergeSources}`); } if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') { $.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`); } if (noCache) { $.info(`指定不使用缓存: ${noCache}`); } if (produceType) { $.info(`指定生产类型: ${produceType}`); } const allFiles = $.read(FILES_KEY); const file = findByName(allFiles, name); if (file) { try { const output = await produceArtifact({ type: 'file', name, url, ua, content, mergeSources, ignoreFailedRemoteFile, $options, proxy, noCache, produceType, all: true, }); try { subInfoUrl = subInfoUrl || file.subInfoUrl; if (subInfoUrl) { // forward flow headers const flowInfo = await getFlowHeaders( subInfoUrl, subInfoUserAgent || file.subInfoUserAgent, undefined, proxy || file.proxy, ); if (flowInfo) { const headers = normalizeFlowHeader(flowInfo, true); if (headers?.['subscription-userinfo']) { res.set( 'subscription-userinfo', headers['subscription-userinfo'], ); } if (headers?.['profile-web-page-url']) { res.set( 'profile-web-page-url', headers['profile-web-page-url'], ); } if (headers?.['plan-name']) { res.set('plan-name', headers['plan-name']); } } } } catch (err) { $.error( `文件 ${name} 获取流量信息时发生错误: ${JSON.stringify( err, )}`, ); } if (file.download) { res.set( 'Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent( file.displayName || file.name, )}`, ); } res.set('Content-Type', 'text/plain; charset=utf-8'); if (output?.$options?._res?.headers) { Object.entries(output.$options._res.headers).forEach( ([key, value]) => { if (value == null) { res.removeHeader(key); } else { res.set(key, value); } }, ); } if (output?.$options?._res?.status) { res.status(output.$options._res.status); } res.send(output?.$content ?? ''); } catch (err) { $.notify( `🌍 Sub-Store 下载文件失败`, `❌ 无法下载文件:${name}!`, `🤔 原因:${err.message ?? err}`, ); $.error(err.message ?? err); failed( res, new InternalServerError( 'INTERNAL_SERVER_ERROR', `Failed to download file: ${name}`, `Reason: ${err.message ?? err}`, ), ); } } else { $.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}!`); failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `File ${name} does not exist!`, ), 404, ); } } function getWholeFile(req, res) { let { name } = req.params; let { raw } = req.query; const allFiles = $.read(FILES_KEY); const file = findByName(allFiles, name); if (file) { if (raw) { res.set('content-type', 'application/json') .set( 'content-disposition', `attachment; filename="${encodeURIComponent( `sub-store_file_${name}_${formatDateTime( new Date(), )}.json`, )}"`, ) .send(JSON.stringify(file)); } else { success(res, file); } } else { failed( res, new ResourceNotFoundError( `FILE_NOT_FOUND`, `File ${name} does not exist`, 404, ), ); } } function updateFile(req, res) { let { name } = req.params; let file = req.body; const allFiles = $.read(FILES_KEY); const oldFile = findByName(allFiles, name); if (oldFile) { if (!file.name) file.name = oldFile.name; const newFile = { ...oldFile, ...file, }; $.info(`正在更新文件:${name}...`); if (name !== newFile.name) { // update all artifacts referring this collection const allArtifacts = $.read(ARTIFACTS_KEY) || []; for (const artifact of allArtifacts) { if ( artifact.type === 'file' && artifact.source === oldFile.name ) { artifact.source = newFile.name; } } $.write(allArtifacts, ARTIFACTS_KEY); } updateByName(allFiles, name, newFile); $.write(allFiles, FILES_KEY); success(res, newFile); } else { failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `File ${name} does not exist!`, ), 404, ); } } function deleteFile(req, res) { let { name } = req.params; $.info(`正在删除文件:${name}`); let allFiles = $.read(FILES_KEY); deleteByName(allFiles, name); $.write(allFiles, FILES_KEY); success(res); } function getAllFiles(req, res) { const allFiles = $.read(FILES_KEY); success( res, // eslint-disable-next-line no-unused-vars allFiles.map(({ content, ...rest }) => rest), ); } function getAllWholeFiles(req, res) { const allFiles = $.read(FILES_KEY); success(res, allFiles); } function replaceFile(req, res) { const allFiles = req.body; $.write(allFiles, FILES_KEY); success(res); } ================================================ FILE: backend/src/restful/index.js ================================================ import { Base64 } from 'js-base64'; import _ from 'lodash'; import express from '@/vendor/express'; import $ from '@/core/app'; import migrate from '@/utils/migration'; import download, { downloadFile } from '@/utils/download'; import { syncArtifacts, produceArtifact } from '@/restful/sync'; import { gistBackupAction } from '@/restful/miscs'; import { TOKENS_KEY, SETTINGS_KEY } from '@/constants'; import registerSubscriptionRoutes from './subscriptions'; import registerCollectionRoutes from './collections'; import registerArtifactRoutes from './artifacts'; import registerFileRoutes from './file'; import registerTokenRoutes from './token'; import registerModuleRoutes from './module'; import registerSyncRoutes from './sync'; import registerDownloadRoutes from './download'; import registerSettingRoutes from './settings'; import registerPreviewRoutes from './preview'; import registerSortingRoutes from './sort'; import registerMiscRoutes from './miscs'; import registerNodeInfoRoutes from './node-info'; import registerParserRoutes from './parser'; export default function serve() { let port; let host; if ($.env.isNode) { port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000; host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::'; } const $app = express({ substore: $, port, host }); if ($.env.isNode) { const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE'); const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX'); const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH'); const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH'); if (be_prefix || be_merge) { if (!fe_be_path.startsWith('/')) { throw new Error( 'SUB_STORE_FRONTEND_BACKEND_PATH should start with /', ); } if (be_merge) { $.info(`[BACKEND] MERGE mode is [ON].`); $.info(`[BACKEND && FRONTEND] ${host}:${port}`); } $.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`); $app.use((req, res, next) => { if (req.path.startsWith(fe_be_path)) { req.url = req.url.replace(fe_be_path, '') || '/'; if (be_merge && req.url.startsWith('/api/')) { req.query['share'] = 'true'; } next(); return; } const pathname = decodeURIComponent(req._parsedUrl.pathname) || '/'; if ( be_merge && req.path.startsWith('/share/') && req.query.token ) { if (req.method.toLowerCase() !== 'get') { res.status(405).send('Method not allowed'); return; } const tokens = $.read(TOKENS_KEY) || []; const token = tokens.find( (t) => t.token === req.query.token && (`/share/${t.type}/${t.name}` === pathname || pathname.startsWith( `/share/${t.type}/${t.name}/`, )) && (t.exp == null || t.exp > Date.now()), ); if (token) { next(); return; } else { const settings = $.read(SETTINGS_KEY); if (settings?.appearanceSetting?.invalidShareFakeNode) { req.query._fakeNode = true; req.url = req.url.replace( /\/share\/.*?\//, '/share/sub/', ); next(); return; } } } const isBackendRoute = /^\/(api|download|share)(\/|$)/.test( req.path, ); if (be_merge && fe_path && !isBackendRoute) { const express_ = eval(`require("express")`); const mime_ = eval(`require("mime-types")`); const path_ = eval(`require("path")`); const fs_ = eval(`require("fs")`); // 检查请求的文件是否真实存在,不存在则返回 index.html(SPA 路由) const filePath = path_.join(fe_path, req.path); if (!fs_.existsSync(filePath)) { req.url = '/index.html'; } const staticFileMiddleware = express_.static(fe_path, { setHeaders: (res, path) => { const type = mime_.contentType(path_.extname(path)); if (type) { res.set('Content-Type', type); } }, }); staticFileMiddleware(req, res, next); return; } res.status(404).end(); return; }); } } // register routes registerCollectionRoutes($app); registerSubscriptionRoutes($app); registerDownloadRoutes($app); registerPreviewRoutes($app); registerSortingRoutes($app); registerSettingRoutes($app); registerArtifactRoutes($app); registerFileRoutes($app); registerTokenRoutes($app); registerModuleRoutes($app); registerSyncRoutes($app); registerNodeInfoRoutes($app); registerMiscRoutes($app); registerParserRoutes($app); $app.start(); if ($.env.isNode) { // Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON const backend_sync_cron = eval( 'process.env.SUB_STORE_BACKEND_SYNC_CRON', ); if (backend_sync_cron) { $.info(`[SYNC CRON] ${backend_sync_cron} enabled`); const { CronJob } = eval(`require("cron")`); new CronJob( backend_sync_cron, async function () { try { $.info(`[SYNC CRON] ${backend_sync_cron} started`); await syncArtifacts(); $.info(`[SYNC CRON] ${backend_sync_cron} finished`); } catch (e) { $.error( `[SYNC CRON] ${backend_sync_cron} error: ${ e.message ?? e }`, ); } }, // onTick null, // onComplete true, // start // 'Asia/Shanghai' // timeZone ); } else { if (eval('process.env.SUB_STORE_BACKEND_CRON')) { $.error( `[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`, ); } if (eval('process.env.SUB_STORE_CRON')) { $.error( `[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`, ); } } // 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b // 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON'); if (produce_cron) { $.info(`[PRODUCE CRON] ${produce_cron} enabled`); const { CronJob } = eval(`require("cron")`); produce_cron .split(/\s*;\s*/) .map((item) => item.trim()) .filter((item) => item.length > 0) .forEach((item) => { const [cron, type, name] = item.split(/\s*,\s*/); $.info(`[PRODUCE CRON] ${type} ${name} ${cron} scheduled`); new CronJob( cron.trim(), async function () { try { $.info( `[PRODUCE CRON] ${type} ${name} ${cron} started`, ); await produceArtifact({ type, name }); $.info( `[PRODUCE CRON] ${type} ${name} ${cron} finished`, ); } catch (e) { $.error( `[PRODUCE CRON] ${type} ${name} ${cron} error: ${ e.message ?? e }`, ); } }, // onTick null, // onComplete true, // start // 'Asia/Shanghai' // timeZone ); }); } const backend_download_cron = eval( 'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON', ); if (backend_download_cron) { $.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`); const { CronJob } = eval(`require("cron")`); new CronJob( backend_download_cron, async function () { try { $.info( `[DOWNLOAD CRON] ${backend_download_cron} started`, ); await gistBackupAction('download'); $.info( `[DOWNLOAD CRON] ${backend_download_cron} finished`, ); } catch (e) { $.error( `[DOWNLOAD CRON] ${backend_download_cron} error: ${ e.message ?? e }`, ); } }, // onTick null, // onComplete true, // start // 'Asia/Shanghai' // timeZone ); } const backend_upload_cron = eval( 'process.env.SUB_STORE_BACKEND_UPLOAD_CRON', ); if (backend_upload_cron) { $.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`); const { CronJob } = eval(`require("cron")`); new CronJob( backend_upload_cron, async function () { try { $.info(`[UPLOAD CRON] ${backend_upload_cron} started`); await gistBackupAction('upload'); $.info(`[UPLOAD CRON] ${backend_upload_cron} finished`); } catch (e) { $.error( `[UPLOAD CRON] ${backend_upload_cron} error: ${ e.message ?? e }`, ); } }, // onTick null, // onComplete true, // start // 'Asia/Shanghai' // timeZone ); } const mmdb_cron = eval('process.env.SUB_STORE_MMDB_CRON'); const countryFile = eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH'); const countryUrl = eval('process.env.SUB_STORE_MMDB_COUNTRY_URL'); const asnFile = eval('process.env.SUB_STORE_MMDB_ASN_PATH'); const asnUrl = eval('process.env.SUB_STORE_MMDB_ASN_URL'); if (mmdb_cron && ((countryFile && countryUrl) || (asnFile && asnUrl))) { $.info(`[MMDB CRON] ${mmdb_cron} enabled`); const { CronJob } = eval(`require("cron")`); new CronJob( mmdb_cron, async function () { try { $.info(`[MMDB CRON] ${mmdb_cron} started`); if (countryFile && countryUrl) { try { $.info( `[MMDB CRON] downloading ${countryUrl} to ${countryFile}`, ); await downloadFile(countryUrl, countryFile); } catch (e) { $.error( `[MMDB CRON] ${countryUrl} download failed: ${ e.message ?? e }`, ); } } if (asnFile && asnUrl) { try { $.info( `[MMDB CRON] downloading ${asnUrl} to ${asnFile}`, ); await downloadFile(asnUrl, asnFile); } catch (e) { $.error( `[MMDB CRON] ${asnUrl} download failed: ${ e.message ?? e }`, ); } } $.info(`[MMDB CRON] ${mmdb_cron} finished`); } catch (e) { $.error( `[MMDB CRON] ${mmdb_cron} error: ${e.message ?? e}`, ); } }, // onTick null, // onComplete true, // start // 'Asia/Shanghai' // timeZone ); } const path = eval(`require("path")`); const fs = eval(`require("fs")`); const data_url = eval('process.env.SUB_STORE_DATA_URL'); const data_url_post = eval('process.env.SUB_STORE_DATA_URL_POST'); const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH'); const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001; const fe_host = eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::'; const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH'); const fe_abs_path = path.resolve( fe_path || path.join(__dirname, 'frontend'), ); const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE'); if (fe_path && !be_merge) { try { fs.accessSync(path.join(fe_abs_path, 'index.html')); } catch (e) { $.error( `[FRONTEND] index.html file not found in ${fe_abs_path}`, ); } const express_ = eval(`require("express")`); const history = eval(`require("connect-history-api-fallback")`); const { createProxyMiddleware } = eval( `require("http-proxy-middleware")`, ); const app = express_(); const staticFileMiddleware = express_.static(fe_path); let be_api = '/api/'; let be_download = '/download/'; let be_share = '/share/'; let be_download_rewrite = ''; let be_api_rewrite = ''; let be_share_rewrite = `${be_share}:type/:name`; let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX') ? fe_be_path : ''; if (fe_be_path) { if (!fe_be_path.startsWith('/')) { throw new Error( 'SUB_STORE_FRONTEND_BACKEND_PATH should start with /', ); } be_api_rewrite = `${ fe_be_path === '/' ? '' : fe_be_path }${be_api}`; be_download_rewrite = `${ fe_be_path === '/' ? '' : fe_be_path }${be_download}`; app.use( be_share_rewrite, createProxyMiddleware({ target: `http://127.0.0.1:${port}${prefix}`, changeOrigin: true, pathRewrite: async (path, req) => { if (req.method.toLowerCase() !== 'get') throw new Error('Method not allowed'); const tokens = $.read(TOKENS_KEY) || []; const token = tokens.find( (t) => t.token === req.query.token && t.type === req.params.type && t.name === req.params.name && (t.exp == null || t.exp > Date.now()), ); if (!token) { const settings = $.read(SETTINGS_KEY); if ( settings?.appearanceSetting ?.invalidShareFakeNode ) { return req.originalUrl .replace( /\/share\/.*?\//, '/share/sub/', ) .replace('?', '?_fakeNode=true&'); } else { return '/404'; } } return req.originalUrl; }, }), ); app.use( be_api_rewrite, createProxyMiddleware({ target: `http://127.0.0.1:${port}${prefix}${be_api}`, pathRewrite: async (path) => { return path.includes('?') ? `${path}&share=true` : `${path}?share=true`; }, }), ); app.use( be_download_rewrite, createProxyMiddleware({ target: `http://127.0.0.1:${port}${prefix}${be_download}`, changeOrigin: true, }), ); } app.use(staticFileMiddleware); app.use( history({ disableDotRule: true, verbose: false, }), ); app.use(staticFileMiddleware); const listener = app.listen(fe_port, fe_host, () => { const { address: fe_address, port: fe_port } = listener.address(); $.info(`[FRONTEND] ${fe_address}:${fe_port}`); if (fe_be_path) { $.info( `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`, ); $.info( `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${be_download}`, ); $.info( `[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`, ); } }); } if (data_url) { $.info(`[BACKEND] downloading data from ${data_url}`); download(data_url) .then(async (content) => { try { content = JSON.parse(Base64.decode(content)); if (!(Object.keys(content.settings).length >= 0)) { throw new Error( '备份文件应该至少包含 settings 字段', ); } } catch (err) { try { content = JSON.parse(content); if (!(Object.keys(content.settings).length >= 0)) { throw new Error( '备份文件应该至少包含 settings 字段', ); } } catch (err) { $.error( `Gist 备份文件校验失败, 无法还原\nReason: ${ err.message ?? err }`, ); throw new Error('Gist 备份文件校验失败, 无法还原'); } } if (data_url_post) { $.info('[BACKEND] executing post-processing script'); eval(data_url_post); } $.write(JSON.stringify(content, null, ` `), '#sub-store'); $.cache = content; $.persistCache(); migrate(); $.info(`[BACKEND] restored data from ${data_url}`); }) .catch((e) => { $.error(`[BACKEND] restore data failed`); console.error(e); throw e; }); } } } ================================================ FILE: backend/src/restful/miscs.js ================================================ import { Base64 } from 'js-base64'; import _ from 'lodash'; import $ from '@/core/app'; import { ENV } from '@/vendor/open-api'; import { failed, success } from '@/restful/response'; import { updateArtifactStore, updateAvatar } from '@/restful/settings'; import resourceCache from '@/utils/resource-cache'; import scriptResourceCache from '@/utils/script-resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache'; import { GIST_BACKUP_FILE_NAME, GIST_BACKUP_KEY, SETTINGS_KEY, } from '@/constants'; import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import Gist from '@/utils/gist'; import migrate from '@/utils/migration'; import env from '@/utils/env'; import { formatDateTime } from '@/utils'; export default function register($app) { // utils $app.get('/api/utils/env', getEnv); // get runtime environment $app.get('/api/utils/backup', gistBackup); // gist backup actions $app.get('/api/utils/refresh', refresh); // Storage management $app.route('/api/storage') .get((req, res) => { res.set('content-type', 'application/json') .set( 'content-disposition', `attachment; filename="${encodeURIComponent( `sub-store_data_${formatDateTime(new Date())}.json`, )}"`, ) .send( $.env.isNode ? JSON.stringify($.cache) : $.read('#sub-store'), ); }) .post((req, res) => { let { content } = req.body; try { content = JSON.parse(Base64.decode(content)); if (!(Object.keys(content.settings).length >= 0)) { throw new Error('备份文件应该至少包含 settings 字段'); } } catch (err) { try { content = JSON.parse(content); if (!(Object.keys(content.settings).length >= 0)) { throw new Error('备份文件应该至少包含 settings 字段'); } } catch (err) { $.error( `备份文件校验失败, 无法还原\nReason: ${ err.message ?? err }`, ); throw new Error('备份文件校验失败, 无法还原'); } } $.write(JSON.stringify(content, null, ` `), '#sub-store'); if ($.env.isNode) { $.cache = content; $.persistCache(); } migrate(); success(res); }); if (ENV().isNode) { $app.get('/', getEnv); } else { // Redirect sub.store to vercel webpage $app.get('/', async (req, res) => { // 302 redirect res.set('location', 'https://sub-store.vercel.app/') .status(302) .end(); }); } // handle preflight request for QX if (ENV().isQX) { $app.options('/', async (req, res) => { res.status(200).end(); }); } $app.all('/', (_, res) => { res.send('Hello from sub-store, made with ❤️ by Peng-YM'); }); } function getEnv(req, res) { if (req.query.share) { env.feature.share = true; } res.set('Content-Type', 'application/json;charset=UTF-8').send( JSON.stringify( { status: 'success', data: { guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068', ...env, }, }, null, 2, ), ); } async function refresh(_, res) { // 1. get GitHub avatar and artifact store await updateAvatar(); await updateArtifactStore(); // 2. clear resource cache resourceCache.revokeAll(); scriptResourceCache.revokeAll(); headersResourceCache.revokeAll(); success(res); } async function gistBackupAction(action, keep, encode) { // read token const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); if (!gistToken) throw new Error('GitHub Token is required for backup!'); const gist = new Gist({ token: gistToken, key: GIST_BACKUP_KEY, syncPlatform, }); let currentContent = $.read('#sub-store'); currentContent = currentContent ? JSON.parse(currentContent) : {}; if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache)); let content; const settings = $.read(SETTINGS_KEY); const updated = settings.syncTime; const encoding = encode || settings.gistUpload || 'base64'; $.info( `Gist backup action: ${action}, keep: ${keep}, encode: ${encode}, settings encode: ${settings.gistUpload}, final encoding: ${encoding}`, ); switch (action) { case 'upload': try { content = $.read('#sub-store'); content = content ? JSON.parse(content) : {}; if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache)); if (encoding === 'plaintext') { content.settings.gistToken = '恢复后请重新设置 GitHub Token'; content = JSON.stringify(content, null, ` `); } else { content = Base64.encode( JSON.stringify(content, null, ` `), ); } $.info(`下载备份, 与本地内容对比...`); const onlineContent = await gist.download( GIST_BACKUP_FILE_NAME, ); if (onlineContent === content) { $.info(`内容一致, 无需上传备份`); return; } } catch (error) { $.error(`${error.message ?? error}`); } // update syncTime settings.syncTime = new Date().getTime(); $.write(settings, SETTINGS_KEY); content = $.read('#sub-store'); content = content ? JSON.parse(content) : {}; if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache)); if (encoding === 'plaintext') { content.settings.gistToken = '恢复后请重新设置 GitHub Token'; content = JSON.stringify(content, null, ` `); } else { content = Base64.encode(JSON.stringify(content, null, ` `)); } $.info(`上传备份中...`); try { await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content }, }); $.info(`上传备份完成`); } catch (err) { // restore syncTime if upload failed settings.syncTime = updated; $.write(settings, SETTINGS_KEY); throw err; } break; case 'download': $.info(`还原备份中...`); content = await gist.download(GIST_BACKUP_FILE_NAME); try { content = JSON.parse(Base64.decode(content)); if (!(Object.keys(content.settings).length >= 0)) { throw new Error('备份文件应该至少包含 settings 字段'); } } catch (err) { try { content = JSON.parse(content); if (!(Object.keys(content.settings).length >= 0)) { throw new Error('备份文件应该至少包含 settings 字段'); } } catch (err) { $.error( `Gist 备份文件校验失败, 无法还原\nReason: ${ err.message ?? err }`, ); throw new Error('Gist 备份文件校验失败, 无法还原'); } } if (keep) { $.info(`保留原有设置 ${keep}`); keep.split(',').forEach((path) => { _.set(content, path, _.get(currentContent, path)); }); } // restore settings $.write(JSON.stringify(content, null, ` `), '#sub-store'); if ($.env.isNode) { $.cache = content; $.persistCache(); } $.info(`perform migration after restoring from gist...`); migrate(); $.info(`migration completed`); $.info(`还原备份完成`); break; } } async function gistBackup(req, res) { const { action, keep, encode } = req.query; // read token const { gistToken } = $.read(SETTINGS_KEY); if (!gistToken) { failed( res, new RequestInvalidError( 'GIST_TOKEN_NOT_FOUND', `GitHub Token is required for backup!`, ), ); } else { try { await gistBackupAction(action, keep, encode); success(res); } catch (err) { $.error( `Failed to ${action} gist data.\nReason: ${err.message ?? err}`, ); failed( res, new InternalServerError( 'BACKUP_FAILED', `Failed to ${action} gist data!`, `Reason: ${err.message ?? err}`, ), ); } } } export { gistBackupAction }; ================================================ FILE: backend/src/restful/module.js ================================================ import { deleteByName, findByName, updateByName } from '@/utils/database'; import { MODULES_KEY } from '@/constants'; import { failed, success } from '@/restful/response'; import $ from '@/core/app'; import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors'; import { hex_md5 } from '@/vendor/md5'; export default function register($app) { if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY); $app.route('/api/module/:name') .get(getModule) .patch(updateModule) .delete(deleteModule); $app.route('/api/modules') .get(getAllModules) .post(createModule) .put(replaceModule); } // module API function createModule(req, res) { const module = req.body; module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`; $.info(`正在创建模块:${module.name}`); const allModules = $.read(MODULES_KEY); if (findByName(allModules, module.name)) { return failed( res, new RequestInvalidError( 'DUPLICATE_KEY', req.body.name ? `已存在 name 为 ${module.name} 的模块` : `已存在相同的模块 请勿重复添加`, ), ); } allModules.push(module); $.write(allModules, MODULES_KEY); success(res, module, 201); } function getModule(req, res) { let { name } = req.params; const allModules = $.read(MODULES_KEY); const module = findByName(allModules, name); if (module) { res.set('Content-Type', 'text/plain; charset=utf-8').send( module.content, ); } else { failed( res, new ResourceNotFoundError( `MODULE_NOT_FOUND`, `Module ${name} does not exist`, 404, ), ); } } function updateModule(req, res) { let { name } = req.params; let module = req.body; const allModules = $.read(MODULES_KEY); const oldModule = findByName(allModules, name); if (oldModule) { const newModule = { ...oldModule, ...module, }; $.info(`正在更新模块:${name}...`); updateByName(allModules, name, newModule); $.write(allModules, MODULES_KEY); success(res, newModule); } else { failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Module ${name} does not exist!`, ), 404, ); } } function deleteModule(req, res) { let { name } = req.params; $.info(`正在删除模块:${name}`); let allModules = $.read(MODULES_KEY); deleteByName(allModules, name); $.write(allModules, MODULES_KEY); success(res); } function getAllModules(req, res) { const allModules = $.read(MODULES_KEY); success( res, // eslint-disable-next-line no-unused-vars allModules.map(({ content, ...rest }) => rest), ); } function replaceModule(req, res) { const allModules = req.body; $.write(allModules, MODULES_KEY); success(res); } ================================================ FILE: backend/src/restful/node-info.js ================================================ import producer from '@/core/proxy-utils/producers'; import { HTTP } from '@/vendor/open-api'; import { failed, success } from '@/restful/response'; import { NetworkError } from '@/restful/errors'; export default function register($app) { $app.post('/api/utils/node-info', getNodeInfo); } async function getNodeInfo(req, res) { const proxy = req.body; const lang = req.query.lang || 'zh-CN'; let shareUrl; try { shareUrl = producer.URI.produce(proxy); } catch (err) { // do nothing } try { const $http = HTTP(); const info = await $http .get({ url: `http://ip-api.com/json/${encodeURIComponent( `${proxy.server}` .trim() .replace(/^\[/, '') .replace(/\]$/, ''), )}?lang=${lang}`, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15', }, }) .then((resp) => { const data = JSON.parse(resp.body); if (data.status !== 'success') { throw new Error(data.message); } // remove unnecessary fields delete data.status; return data; }); success(res, { shareUrl, info, }); } catch (err) { failed( res, new NetworkError( 'FAILED_TO_GET_NODE_INFO', `Failed to get node info`, `Reason: ${err}`, ), ); } } ================================================ FILE: backend/src/restful/parser.js ================================================ import { success, failed } from '@/restful/response'; import { ProxyUtils } from '@/core/proxy-utils'; import { RuleUtils } from '@/core/rule-utils'; export default function register($app) { $app.route('/api/proxy/parse').post(proxy_parser); $app.route('/api/rule/parse').post(rule_parser); } /*** * 感谢 izhangxm 的 PR! * 目前没有节点操作, 没有支持完整参数, 以后再完善一下 */ /*** * 代理服务器协议转换接口。 * 请求方法为POST,数据为json。需要提供data和client字段。 * data: string, 协议数据,每行一个或者是clash * client: string, 目标平台名称,见backend/src/core/proxy-utils/producers/index.js * */ function proxy_parser(req, res) { const { data, client, content, platform } = req.body; var result = {}; try { var proxies = ProxyUtils.parse(data ?? content); var par_res = ProxyUtils.produce(proxies, client ?? platform); result['par_res'] = par_res; } catch (err) { failed(res, err); return; } success(res, result); } /** * 规则转换接口。 * 请求方法为POST,数据为json。需要提供data和client字段。 * data: string, 多行规则字符串 * client: string, 目标平台名称,具体见backend/src/core/rule-utils/producers.js */ function rule_parser(req, res) { const { data, client, content, platform } = req.body; var result = {}; try { const rules = RuleUtils.parse(data ?? content); var par_res = RuleUtils.produce(rules, client ?? platform); result['par_res'] = par_res; } catch (err) { failed(res, err); return; } success(res, result); } ================================================ FILE: backend/src/restful/preview.js ================================================ import { InternalServerError } from './errors'; import { ProxyUtils } from '@/core/proxy-utils'; import { findByName } from '@/utils/database'; import { success, failed } from './response'; import download from '@/utils/download'; import { SUBS_KEY } from '@/constants'; import $ from '@/core/app'; export default function register($app) { $app.post('/api/preview/sub', compareSub); $app.post('/api/preview/collection', compareCollection); $app.post('/api/preview/file', previewFile); } async function previewFile(req, res) { try { const file = req.body; let content = ''; if (file.type !== 'mihomoProfile') { if ( file.source === 'local' && !['localFirst', 'remoteFirst'].includes(file.mergeSources) ) { content = file.content; } else { const errors = {}; content = await Promise.all( file.url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, file.ua, undefined, file.proxy, ); } catch (err) { errors[url] = err; $.error( `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`, ); return ''; } }), ); if (Object.keys(errors).length > 0) { if (!file.ignoreFailedRemoteFile) { throw new Error( `文件 ${file.name} 的远程文件 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } else if (file.ignoreFailedRemoteFile === 'enabled') { $.notify( `🌍 Sub-Store 预览文件失败`, `❌ ${file.name}`, `远程文件 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (file.mergeSources === 'localFirst') { content.unshift(file.content); } else if (file.mergeSources === 'remoteFirst') { content.push(file.content); } } } // parse proxies const files = (Array.isArray(content) ? content : [content]).flat(); let filesContent = files .filter((i) => i != null && i !== '') .join('\n'); // apply processors const processed = Array.isArray(file.process) && file.process.length > 0 ? await ProxyUtils.process( { $files: files, $content: filesContent, $file: file }, file.process, ) : { $content: filesContent, $files: files }; // produce success(res, { original: filesContent, processed: processed?.$content ?? '', }); } catch (err) { $.error(err.message ?? err); failed( res, new InternalServerError( `INTERNAL_SERVER_ERROR`, `Failed to preview file`, `Reason: ${err.message ?? err}`, ), ); } } async function compareSub(req, res) { try { const sub = req.body; const target = req.query.target || 'JSON'; let content; if ( sub.source === 'local' && !['localFirst', 'remoteFirst'].includes(sub.mergeSources) ) { content = sub.content; } else { const errors = {}; content = await Promise.all( sub.url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, sub.ua, undefined, sub.proxy, undefined, undefined, undefined, true, ); } catch (err) { errors[url] = err; $.error( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`, ); return ''; } }), ); if (Object.keys(errors).length > 0) { if (!sub.ignoreFailedRemoteSub) { throw new Error( `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } else if (sub.ignoreFailedRemoteSub === 'enabled') { $.notify( `🌍 Sub-Store 预览订阅失败`, `❌ ${sub.name}`, `远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (sub.mergeSources === 'localFirst') { content.unshift(sub.content); } else if (sub.mergeSources === 'remoteFirst') { content.push(sub.content); } } // parse proxies const original = (Array.isArray(content) ? content : [content]) .map((i) => ProxyUtils.parse(i)) .flat(); // add id original.forEach((proxy, i) => { proxy.id = i; proxy._subName = sub.name; proxy._subDisplayName = sub.displayName; }); // apply processors const processed = await ProxyUtils.process( original, sub.process || [], target, { [sub.name]: sub }, ); // produce success(res, { original, processed }); } catch (err) { $.error(err.message ?? err); failed( res, new InternalServerError( `INTERNAL_SERVER_ERROR`, `Failed to preview subscription`, `Reason: ${err.message ?? err}`, ), ); } } async function compareCollection(req, res) { try { const allSubs = $.read(SUBS_KEY); const collection = req.body; const subnames = [...collection.subscriptions]; let subscriptionTags = collection.subscriptionTags; if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) { allSubs.forEach((sub) => { if ( Array.isArray(sub.tag) && sub.tag.length > 0 && !subnames.includes(sub.name) && sub.tag.some((tag) => subscriptionTags.includes(tag)) ) { subnames.push(sub.name); } }); } const results = {}; const errors = {}; await Promise.all( subnames.map(async (name) => { const sub = findByName(allSubs, name); try { let raw; if ( sub.source === 'local' && !['localFirst', 'remoteFirst'].includes( sub.mergeSources, ) ) { raw = sub.content; } else { const errors = {}; raw = await Promise.all( sub.url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, sub.ua, undefined, sub.proxy, undefined, undefined, undefined, true, ); } catch (err) { errors[url] = err; $.error( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`, ); return ''; } }), ); if (Object.keys(errors).length > 0) { if (!sub.ignoreFailedRemoteSub) { throw new Error( `订阅 ${sub.name} 的远程订阅 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } else if ( sub.ignoreFailedRemoteSub === 'enabled' ) { $.notify( `🌍 Sub-Store 预览订阅失败`, `❌ ${sub.name}`, `远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (sub.mergeSources === 'localFirst') { raw.unshift(sub.content); } else if (sub.mergeSources === 'remoteFirst') { raw.push(sub.content); } } // parse proxies let currentProxies = (Array.isArray(raw) ? raw : [raw]) .map((i) => ProxyUtils.parse(i)) .flat(); currentProxies.forEach((proxy) => { proxy._subName = sub.name; proxy._subDisplayName = sub.displayName; proxy._collectionName = collection.name; proxy._collectionDisplayName = collection.displayName; }); // apply processors currentProxies = await ProxyUtils.process( currentProxies, sub.process || [], 'JSON', { [sub.name]: sub, _collection: collection }, ); results[name] = currentProxies; } catch (err) { errors[name] = err; $.error( `❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name} 时出现错误:${err}!`, ); } }), ); if (Object.keys(errors).length > 0) { if (!collection.ignoreFailedRemoteSub) { throw new Error( `组合订阅 ${collection.name} 的子订阅 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } else if (collection.ignoreFailedRemoteSub === 'enabled') { $.notify( `🌍 Sub-Store 预览组合订阅失败`, `❌ ${collection.name}`, `子订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } // merge proxies with the original order const original = Array.prototype.concat.apply( [], subnames.map((name) => results[name] || []), ); original.forEach((proxy, i) => { proxy.id = i; proxy._collectionName = collection.name; proxy._collectionDisplayName = collection.displayName; }); const processed = await ProxyUtils.process( original, collection.process || [], 'JSON', { _collection: collection }, ); success(res, { original, processed }); } catch (err) { $.error(err.message ?? err); failed( res, new InternalServerError( `INTERNAL_SERVER_ERROR`, `Failed to preview collection`, `Reason: ${err.message ?? err}`, ), ); } } ================================================ FILE: backend/src/restful/response.js ================================================ export function success(resp, data, statusCode) { resp.status(statusCode || 200).json({ status: 'success', data, }); } export function failed(resp, error, statusCode) { resp.status(statusCode || 500).json({ status: 'failed', error: { code: error.code, type: error.type, message: error.message, details: resp.req?.route?.path?.startsWith('/share/') ? '详情请查看日志' : error.details, }, }); } ================================================ FILE: backend/src/restful/settings.js ================================================ import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants'; import { success, failed } from './response'; import { InternalServerError } from '@/restful/errors'; import $ from '@/core/app'; import Gist from '@/utils/gist'; export default function register($app) { const settings = $.read(SETTINGS_KEY); if (!settings) $.write({}, SETTINGS_KEY); $app.route('/api/settings').get(getSettings).patch(updateSettings); } async function getSettings(req, res) { try { let settings = $.read(SETTINGS_KEY); if (!settings) { settings = {}; $.write(settings, SETTINGS_KEY); } if (!settings.avatarUrl) await updateAvatar(); if (!settings.artifactStore) await updateArtifactStore(); success(res, settings); } catch (e) { $.error(`Failed to get settings: ${e.message ?? e}`); failed( res, new InternalServerError( `FAILED_TO_GET_SETTINGS`, `Failed to get settings`, `Reason: ${e.message ?? e}`, ), ); } } async function updateSettings(req, res) { try { const settings = $.read(SETTINGS_KEY); const newSettings = { ...settings, ...req.body, }; [ 'defaultTimeout', 'cacheThreshold', 'resourceCacheTtl', 'headersCacheTtl', 'scriptCacheTtl', ].map((key) => { let value = Number(newSettings[key]); if (!isFinite(value) || value <= 0) { delete newSettings[key]; } }); $.write(newSettings, SETTINGS_KEY); if ( req.body.githubUser || req.body.gistToken || req.body.githubProxy || req.body.defaultProxy ) { await updateAvatar(); await updateArtifactStore(); } success(res, newSettings); } catch (e) { $.error(`Failed to update settings: ${e.message ?? e}`); failed( res, new InternalServerError( `FAILED_TO_UPDATE_SETTINGS`, `Failed to update settings`, `Reason: ${e.message ?? e}`, ), ); } } export async function updateAvatar() { const settings = $.read(SETTINGS_KEY); const { githubUser: username, syncPlatform, githubProxy } = settings; if (username) { if (syncPlatform === 'gitlab') { try { const data = await $.http .get({ url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent( username, )}`, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', }, }) .then((resp) => JSON.parse(resp.body)); settings.avatarUrl = data[0]['avatar_url'].replace( /(\?|&)s=\d+(&|$)/, '$1s=160$2', ); $.write(settings, SETTINGS_KEY); } catch (err) { $.error( `Failed to fetch GitLab avatar for User: ${username}. Reason: ${ err.message ?? err }`, ); } } else { try { const data = await $.http .get({ url: `${ githubProxy ? `${githubProxy}/` : '' }https://api.github.com/users/${encodeURIComponent( username, )}`, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', }, }) .then((resp) => JSON.parse(resp.body)); settings.avatarUrl = data['avatar_url']; $.write(settings, SETTINGS_KEY); } catch (err) { $.error( `Failed to fetch GitHub avatar for User: ${username}. Reason: ${ err.message ?? err }`, ); } } } } export async function updateArtifactStore() { $.log('Updating artifact store'); const settings = $.read(SETTINGS_KEY); const { gistToken, syncPlatform } = settings; if (gistToken) { const manager = new Gist({ token: gistToken, key: ARTIFACT_REPOSITORY_KEY, syncPlatform, }); try { const gist = await manager.locate(); const url = gist?.html_url ?? gist?.web_url; if (url) { $.log(`找到 Sub-Store Gist: ${url}`); // 只需要保证 token 是对的, 现在 username 错误只会导致头像错误 settings.artifactStore = url; settings.artifactStoreStatus = 'VALID'; } else { $.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`); settings.artifactStoreStatus = 'NOT FOUND'; } } catch (err) { $.error( `查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${ err.message ?? err }`, ); settings.artifactStoreStatus = 'ERROR'; } $.write(settings, SETTINGS_KEY); } } ================================================ FILE: backend/src/restful/sort.js ================================================ import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY, FILES_KEY, TOKENS_KEY, } from '@/constants'; import $ from '@/core/app'; import { success } from '@/restful/response'; export default function register($app) { $app.post('/api/sort/subs', sortSubs); $app.post('/api/sort/collections', sortCollections); $app.post('/api/sort/artifacts', sortArtifacts); $app.post('/api/sort/files', sortFiles); $app.post('/api/sort/tokens', sortTokens); } function sortSubs(req, res) { const orders = req.body; const allSubs = $.read(SUBS_KEY); allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name)); $.write(allSubs, SUBS_KEY); success(res, allSubs); } function sortCollections(req, res) { const orders = req.body; const allCols = $.read(COLLECTIONS_KEY); allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name)); $.write(allCols, COLLECTIONS_KEY); success(res, allCols); } function sortArtifacts(req, res) { const orders = req.body; const allArtifacts = $.read(ARTIFACTS_KEY); allArtifacts.sort( (a, b) => orders.indexOf(a.name) - orders.indexOf(b.name), ); $.write(allArtifacts, ARTIFACTS_KEY); success(res, allArtifacts); } function sortFiles(req, res) { const orders = req.body; const allFiles = $.read(FILES_KEY); allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name)); $.write(allFiles, FILES_KEY); success(res, allFiles); } function sortTokens(req, res) { const orders = req.body; const allTokens = $.read(TOKENS_KEY); allTokens.sort( (a, b) => orders.indexOf(`${a.type}-${a.name}-${a.token}`) - orders.indexOf(`${b.type}-${b.name}-${b.token}`), ); $.write(allTokens, TOKENS_KEY); success(res, allTokens); } ================================================ FILE: backend/src/restful/subscriptions.js ================================================ import { NetworkError, InternalServerError, ResourceNotFoundError, RequestInvalidError, } from './errors'; import { deleteByName, findByName, updateByName } from '@/utils/database'; import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY, } from '@/constants'; import { getFlowHeaders, parseFlowHeaders, getRmainingDays, } from '@/utils/flow'; import { success, failed } from './response'; import $ from '@/core/app'; import { formatDateTime } from '@/utils'; if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY); export default function register($app) { $app.get('/api/sub/flow/:name', getFlowInfo); $app.route('/api/sub/:name') .get(getSubscription) .patch(updateSubscription) .delete(deleteSubscription); $app.route('/api/subs') .get(getAllSubscriptions) .post(createSubscription) .put(replaceSubscriptions); } // subscriptions API async function getFlowInfo(req, res) { let { name } = req.params; let { url } = req.query; if (url) { $.info(`指定远程订阅 URL: ${url}`); } const allSubs = $.read(SUBS_KEY); const sub = findByName(allSubs, name); if (!sub) { failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Subscription ${name} does not exist!`, ), 404, ); return; } if ( sub.source === 'local' && !['localFirst', 'remoteFirst'].includes(sub.mergeSources) ) { if (sub.subUserinfo) { let subUserInfo; if (/^https?:\/\//.test(sub.subUserinfo)) { try { subUserInfo = await getFlowHeaders( undefined, undefined, undefined, sub.proxy, sub.subUserinfo, ); } catch (e) { $.error( `订阅 ${name} 使用自定义流量链接 ${ sub.subUserinfo } 获取流量信息时发生错误: ${JSON.stringify(e)}`, ); } } else { subUserInfo = sub.subUserinfo; } try { success(res, { ...parseFlowHeaders(subUserInfo), }); } catch (e) { $.error( `Failed to parse flow info for local subscription ${name}: ${ e.message ?? e }`, ); failed( res, new RequestInvalidError( 'NO_FLOW_INFO', 'N/A', `Failed to parse flow info`, ), ); } } else { failed( res, new RequestInvalidError( 'NO_FLOW_INFO', 'N/A', `Local subscription ${name} has no flow information!`, ), ); } return; } try { url = `${url || sub.url}` .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length)?.[0] || ''; let $arguments = {}; const rawArgs = url.split('#'); url = url.split('#')[0]; if (rawArgs.length > 1) { try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` $arguments = JSON.parse(decodeURIComponent(rawArgs[1])); } catch (e) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; $arguments[key] = value == null || value === '' ? true : decodeURIComponent(value); } } } if ($arguments.noFlow || !/^https?/.test(url)) { failed( res, new RequestInvalidError( 'NO_FLOW_INFO', 'N/A', `Subscription ${name}: noFlow`, ), ); return; } const flowHeaders = await getFlowHeaders( $arguments?.insecure ? `${url}#insecure` : url, $arguments.flowUserAgent, undefined, sub.proxy, $arguments.flowUrl, ); if (!flowHeaders && !sub.subUserinfo) { failed( res, new InternalServerError( 'NO_FLOW_INFO', 'No flow info', `Failed to fetch flow headers`, ), ); return; } try { const remainingDays = getRmainingDays({ resetDay: $arguments.resetDay, startDate: $arguments.startDate, cycleDays: $arguments.cycleDays, }); let subUserInfo; if (/^https?:\/\//.test(sub.subUserinfo)) { try { subUserInfo = await getFlowHeaders( undefined, undefined, undefined, sub.proxy, sub.subUserinfo, ); } catch (e) { $.error( `订阅 ${name} 使用自定义流量链接 ${ sub.subUserinfo } 获取流量信息时发生错误: ${JSON.stringify(e)}`, ); } } else { subUserInfo = sub.subUserinfo; } const result = { ...parseFlowHeaders( [subUserInfo, flowHeaders].filter((i) => i).join('; '), ), }; if (remainingDays != null) { result.remainingDays = remainingDays; } success(res, result); } catch (e) { $.error( `Failed to parse flow info for local subscription ${name}: ${ e.message ?? e }`, ); failed( res, new RequestInvalidError( 'NO_FLOW_INFO', 'N/A', `Failed to parse flow info`, ), ); } } catch (err) { failed( res, new NetworkError( `URL_NOT_ACCESSIBLE`, `The URL for subscription ${name} is inaccessible.`, ), ); } } function createSubscription(req, res) { const sub = req.body; delete sub.subscriptions; $.info(`正在创建订阅: ${sub.name}`); if (/\//.test(sub.name)) { failed( res, new RequestInvalidError( 'INVALID_NAME', `Subscription ${sub.name} is invalid`, ), ); return; } const allSubs = $.read(SUBS_KEY); if (findByName(allSubs, sub.name)) { failed( res, new RequestInvalidError( 'DUPLICATE_KEY', `Subscription ${sub.name} already exists.`, ), ); return; } allSubs.push(sub); $.write(allSubs, SUBS_KEY); success(res, sub, 201); } function getSubscription(req, res) { let { name } = req.params; let { raw } = req.query; const allSubs = $.read(SUBS_KEY); const sub = findByName(allSubs, name); delete sub.subscriptions; if (sub) { if (raw) { res.set('content-type', 'application/json') .set( 'content-disposition', `attachment; filename="${encodeURIComponent( `sub-store_subscription_${name}_${formatDateTime( new Date(), )}.json`, )}"`, ) .send(JSON.stringify(sub)); } else { success(res, sub); } } else { failed( res, new ResourceNotFoundError( `SUBSCRIPTION_NOT_FOUND`, `Subscription ${name} does not exist`, 404, ), ); } } function updateSubscription(req, res) { let { name } = req.params; let sub = req.body; delete sub.subscriptions; const allSubs = $.read(SUBS_KEY); const oldSub = findByName(allSubs, name); if (oldSub) { if (!sub.name) sub.name = oldSub.name; const newSub = { ...oldSub, ...sub, }; $.info(`正在更新订阅: ${name}`); // allow users to update the subscription name if (name !== sub.name) { // update all collections refer to this name const allCols = $.read(COLLECTIONS_KEY) || []; for (const collection of allCols) { const idx = collection.subscriptions.indexOf(name); if (idx !== -1) { collection.subscriptions[idx] = sub.name; } } // update all artifacts referring this subscription const allArtifacts = $.read(ARTIFACTS_KEY) || []; for (const artifact of allArtifacts) { if ( artifact.type === 'subscription' && artifact.source == name ) { artifact.source = sub.name; } } // update all files referring this subscription const allFiles = $.read(FILES_KEY) || []; for (const file of allFiles) { if ( file.sourceType === 'subscription' && file.sourceName == name ) { file.sourceName = sub.name; } } $.write(allCols, COLLECTIONS_KEY); $.write(allArtifacts, ARTIFACTS_KEY); $.write(allFiles, FILES_KEY); } updateByName(allSubs, name, newSub); $.write(allSubs, SUBS_KEY); success(res, newSub); } else { failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `Subscription ${name} does not exist!`, ), 404, ); } } function deleteSubscription(req, res) { let { name } = req.params; $.info(`删除订阅:${name}...`); // delete from subscriptions let allSubs = $.read(SUBS_KEY); deleteByName(allSubs, name); $.write(allSubs, SUBS_KEY); // delete from collections const allCols = $.read(COLLECTIONS_KEY); for (const collection of allCols) { collection.subscriptions = collection.subscriptions.filter( (s) => s !== name, ); } $.write(allCols, COLLECTIONS_KEY); success(res); } function getAllSubscriptions(req, res) { const allSubs = $.read(SUBS_KEY); success(res, allSubs); } function replaceSubscriptions(req, res) { const allSubs = req.body; $.write(allSubs, SUBS_KEY); success(res); } ================================================ FILE: backend/src/restful/sync.js ================================================ import $ from '@/core/app'; import { ARTIFACTS_KEY, COLLECTIONS_KEY, RULES_KEY, SUBS_KEY, FILES_KEY, } from '@/constants'; import { failed, success } from '@/restful/response'; import { InternalServerError, ResourceNotFoundError } from '@/restful/errors'; import { findByName } from '@/utils/database'; import download from '@/utils/download'; import { ProxyUtils } from '@/core/proxy-utils'; import { RuleUtils } from '@/core/rule-utils'; import { syncToGist } from '@/restful/artifacts'; export default function register($app) { // Initialization if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY); // sync all artifacts $app.get('/api/sync/artifacts', syncAllArtifacts); $app.get('/api/sync/artifact/:name', syncArtifact); } async function produceArtifact({ type, name, platform, url, ua, content, mergeSources, ignoreFailedRemoteSub, ignoreFailedRemoteFile, produceType, produceOpts = {}, subscription, awaitCustomCache, $options, proxy, noCache, all, }) { platform = platform || 'JSON'; if (['subscription', 'sub'].includes(type)) { let sub; if (name) { const allSubs = $.read(SUBS_KEY); sub = findByName(allSubs, name); if (!sub) throw new Error(`找不到订阅 ${name}`); } else if (subscription) { sub = subscription; } else { throw new Error('未提供订阅名称或订阅数据'); } let raw; if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) { raw = content; } else if (url) { const errors = {}; raw = await Promise.all( url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, ua || sub.ua, undefined, proxy || sub.proxy, undefined, awaitCustomCache, noCache || sub.noCache, true, ); } catch (err) { errors[url] = err; $.error( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`, ); return ''; } }), ); let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub; if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { subIgnoreFailedRemoteSub = ignoreFailedRemoteSub; } if (Object.keys(errors).length > 0) { if (!subIgnoreFailedRemoteSub) { throw new Error( `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } else if (subIgnoreFailedRemoteSub === 'enabled') { $.notify( `🌍 Sub-Store 处理订阅失败`, `❌ ${sub.name}`, `远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (mergeSources === 'localFirst') { raw.unshift(content); } else if (mergeSources === 'remoteFirst') { raw.push(content); } } else if ( sub.source === 'local' && !['localFirst', 'remoteFirst'].includes(sub.mergeSources) ) { raw = sub.content; } else { const errors = {}; raw = await Promise.all( sub.url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, ua || sub.ua, undefined, proxy || sub.proxy, undefined, awaitCustomCache, noCache || sub.noCache, true, ); } catch (err) { errors[url] = err; $.error( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`, ); return ''; } }), ); let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub; if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { subIgnoreFailedRemoteSub = ignoreFailedRemoteSub; } if (Object.keys(errors).length > 0) { if (!subIgnoreFailedRemoteSub) { throw new Error( `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } else if (subIgnoreFailedRemoteSub === 'enabled') { $.notify( `🌍 Sub-Store 处理订阅失败`, `❌ ${sub.name}`, `远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (sub.mergeSources === 'localFirst') { raw.unshift(sub.content); } else if (sub.mergeSources === 'remoteFirst') { raw.push(sub.content); } } if (produceType === 'raw') { return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat()); } // parse proxies let proxies = (Array.isArray(raw) ? raw : [raw]) .map((i) => ProxyUtils.parse(i)) .flat(); proxies.forEach((proxy) => { proxy._subName = sub.name; proxy._subDisplayName = sub.displayName; }); // apply processors proxies = await ProxyUtils.process( proxies, sub.process || [], platform, { [sub.name]: sub }, $options, ); if (proxies.length === 0) { throw new Error(`订阅 ${name} 中不含有效节点`); } // check duplicate const exist = {}; for (const proxy of proxies) { if (exist[proxy.name]) { $.notify( '🌍 Sub-Store', `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}!`, '请仔细检测配置!', { 'media-url': 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png', }, ); break; } exist[proxy.name] = true; } // produce return ProxyUtils.produce(proxies, platform, produceType, produceOpts); } else if (['collection', 'col'].includes(type)) { const allSubs = $.read(SUBS_KEY); const allCols = $.read(COLLECTIONS_KEY); const collection = findByName(allCols, name); if (!collection) throw new Error(`找不到组合订阅 ${name}`); const subnames = [...collection.subscriptions]; let subscriptionTags = collection.subscriptionTags; if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) { allSubs.forEach((sub) => { if ( Array.isArray(sub.tag) && sub.tag.length > 0 && !subnames.includes(sub.name) && sub.tag.some((tag) => subscriptionTags.includes(tag)) ) { subnames.push(sub.name); } }); } const results = {}; const errors = {}; let processed = 0; await Promise.all( subnames.map(async (name) => { const sub = findByName(allSubs, name); const passThroughUA = sub.passThroughUA; let reqUA = sub.ua; if (passThroughUA) { $.info( `订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`, ); reqUA = ua; } try { $.info(`正在处理子订阅:${sub.name}...`); let raw; if ( sub.source === 'local' && !['localFirst', 'remoteFirst'].includes( sub.mergeSources, ) ) { raw = sub.content; } else { const errors = {}; raw = await await Promise.all( sub.url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, reqUA, undefined, proxy || sub.proxy || collection.proxy, undefined, undefined, noCache || sub.noCache, true, ); } catch (err) { errors[url] = err; $.error( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`, ); return ''; } }), ); if (Object.keys(errors).length > 0) { if (!sub.ignoreFailedRemoteSub) { throw new Error( `订阅 ${sub.name} 的远程订阅 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } else if ( sub.ignoreFailedRemoteSub === 'enabled' ) { $.notify( `🌍 Sub-Store 处理订阅失败`, `❌ ${sub.name}`, `远程订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (sub.mergeSources === 'localFirst') { raw.unshift(sub.content); } else if (sub.mergeSources === 'remoteFirst') { raw.push(sub.content); } } // parse proxies let currentProxies = (Array.isArray(raw) ? raw : [raw]) .map((i) => ProxyUtils.parse(i)) .flat(); currentProxies.forEach((proxy) => { proxy._subName = sub.name; proxy._subDisplayName = sub.displayName; proxy._collectionName = collection.name; proxy._collectionDisplayName = collection.displayName; }); // apply processors currentProxies = await ProxyUtils.process( currentProxies, sub.process || [], platform, { [sub.name]: sub, _collection: collection, $options, }, ); results[name] = currentProxies; processed++; $.info( `✅ 子订阅:${sub.name}加载成功,进度--${ 100 * (processed / subnames.length).toFixed(1) }% `, ); } catch (err) { processed++; errors[name] = err; $.error( `❌ 处理组合订阅中的子订阅: ${ sub.name }时出现错误:${err}!进度--${ 100 * (processed / subnames.length).toFixed(1) }%`, ); } }), ); let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub; if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub; } if (Object.keys(errors).length > 0) { if (!collectionIgnoreFailedRemoteSub) { throw new Error( `组合订阅 ${collection.name} 的子订阅 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } else if (collectionIgnoreFailedRemoteSub === 'enabled') { $.notify( `🌍 Sub-Store 处理组合订阅失败`, `❌ ${collection.name}`, `子订阅 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } // merge proxies with the original order let proxies = Array.prototype.concat.apply( [], subnames.map((name) => results[name] || []), ); proxies.forEach((proxy) => { proxy._collectionName = collection.name; proxy._collectionDisplayName = collection.displayName; }); // apply own processors proxies = await ProxyUtils.process( proxies, collection.process || [], platform, { _collection: collection }, $options, ); if (proxies.length === 0) { throw new Error(`组合订阅 ${name} 中不含有效节点`); } // check duplicate const exist = {}; for (const proxy of proxies) { if (exist[proxy.name]) { $.notify( '🌍 Sub-Store', `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}!`, '请仔细检测配置!', { 'media-url': 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png', }, ); break; } exist[proxy.name] = true; } return ProxyUtils.produce(proxies, platform, produceType, produceOpts); } else if (type === 'rule') { const allRules = $.read(RULES_KEY); const rule = findByName(allRules, name); if (!rule) throw new Error(`找不到规则 ${name}`); let rules = []; for (let i = 0; i < rule.urls.length; i++) { const url = rule.urls[i]; $.info( `正在处理URL:${url},进度--${ 100 * ((i + 1) / rule.urls.length).toFixed(1) }% `, ); try { const { body } = await download(url); const currentRules = RuleUtils.parse(body); rules = rules.concat(currentRules); } catch (err) { $.error( `处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`, ); } } // remove duplicates rules = await RuleUtils.process(rules, [ { type: 'Remove Duplicate Filter' }, ]); // produce output return RuleUtils.produce(rules, platform); } else if (type === 'file') { const allFiles = $.read(FILES_KEY); const file = findByName(allFiles, name); if (!file) throw new Error(`找不到文件 ${name}`); let raw = ''; if (file.type !== 'mihomoProfile') { if ( content && !['localFirst', 'remoteFirst'].includes(mergeSources) ) { raw = content; } else if (url) { const errors = {}; raw = await Promise.all( url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, ua || file.ua, undefined, file.proxy || proxy, undefined, undefined, noCache, ); } catch (err) { errors[url] = err; $.error( `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`, ); return ''; } }), ); let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile; if ( ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '' ) { fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile; } if ( !fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0 ) { throw new Error( `文件 ${file.name} 的远程文件 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } if (mergeSources === 'localFirst') { raw.unshift(content); } else if (mergeSources === 'remoteFirst') { raw.push(content); } } else if ( file.source === 'local' && !['localFirst', 'remoteFirst'].includes(file.mergeSources) ) { raw = file.content; } else { const errors = {}; raw = await Promise.all( file.url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length) .map(async (url) => { try { return await download( url, ua || file.ua, undefined, file.proxy || proxy, undefined, undefined, noCache, ); } catch (err) { errors[url] = err; $.error( `文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`, ); return ''; } }), ); let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile; if ( ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '' ) { fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile; } if (Object.keys(errors).length > 0) { if (!fileIgnoreFailedRemoteFile) { throw new Error( `文件 ${file.name} 的远程文件 ${Object.keys( errors, ).join(', ')} 发生错误, 请查看日志`, ); } else if (fileIgnoreFailedRemoteFile === 'enabled') { $.notify( `🌍 Sub-Store 处理文件失败`, `❌ ${file.name}`, `远程文件 ${Object.keys(errors).join( ', ', )} 发生错误, 请查看日志`, ); } } if (file.mergeSources === 'localFirst') { raw.unshift(file.content); } else if (file.mergeSources === 'remoteFirst') { raw.push(file.content); } } } if (produceType === 'raw') { return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat()); } const files = (Array.isArray(raw) ? raw : [raw]).flat(); let filesContent = files .filter((i) => i != null && i !== '') .join('\n'); // apply processors const processed = Array.isArray(file.process) && file.process.length > 0 ? await ProxyUtils.process( { $files: files, $content: filesContent, $options, $file: file, }, file.process, ) : { $content: filesContent, $files: files, $options }; return (all ? processed : processed?.$content) ?? ''; } } async function syncArtifacts() { $.info('开始同步所有远程配置...'); const allArtifacts = $.read(ARTIFACTS_KEY); const files = {}; try { const valid = []; const invalid = []; const allSubs = $.read(SUBS_KEY); const allCols = $.read(COLLECTIONS_KEY); const subNames = []; let enabledCount = 0; allArtifacts.map((artifact) => { if (artifact.sync && artifact.source) { enabledCount++; if (artifact.type === 'subscription') { const subName = artifact.source; const sub = findByName(allSubs, subName); if (sub && sub.url && !subNames.includes(subName)) { subNames.push(subName); } } else if (artifact.type === 'collection') { const collection = findByName(allCols, artifact.source); if (collection && Array.isArray(collection.subscriptions)) { collection.subscriptions.map((subName) => { const sub = findByName(allSubs, subName); if (sub && sub.url && !subNames.includes(subName)) { subNames.push(subName); } }); } } } }); if (enabledCount === 0) { $.info( `需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`, ); return; } if (subNames.length > 0) { await Promise.all( subNames.map(async (subName) => { try { await produceArtifact({ type: 'subscription', name: subName, awaitCustomCache: true, }); } catch (e) { // $.error(`${e.message ?? e}`); } }), ); } await Promise.all( allArtifacts.map(async (artifact) => { try { if (artifact.sync && artifact.source) { $.info(`正在同步云配置:${artifact.name}...`); const useMihomoExternal = artifact.platform === 'SurgeMac'; if (useMihomoExternal) { $.info( `手动指定了 target 为 SurgeMac, 将使用 Mihomo External`, ); } const output = await produceArtifact({ type: artifact.type, name: artifact.source, platform: artifact.platform, produceOpts: { 'include-unsupported-proxy': artifact.includeUnsupportedProxy, useMihomoExternal, }, }); // if (!output || output.length === 0) // throw new Error('该配置的结果为空 不进行上传'); files[encodeURIComponent(artifact.name)] = { content: output, }; valid.push(artifact.name); } } catch (e) { $.error( `生成同步配置 ${artifact.name} 发生错误: ${ e.message ?? e }`, ); invalid.push(artifact.name); } }), ); $.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`); $.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`); if (valid.length === 0) { throw new Error( `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`, ); } const resp = await syncToGist(files); const body = JSON.parse(resp.body); delete body.history; delete body.forks; delete body.owner; Object.values(body.files).forEach((file) => { delete file.content; }); $.info('上传配置响应:'); $.info(JSON.stringify(body, null, 2)); for (const artifact of allArtifacts) { if ( artifact.sync && artifact.source && valid.includes(artifact.name) ) { artifact.updated = new Date().getTime(); // extract real url from gist let files = body.files; let isGitLab; if (Array.isArray(files)) { isGitLab = true; files = Object.fromEntries( files.map((item) => [item.path, item]), ); } const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url; const new_url = isGitLab ? raw_url : raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); $.info( `上传配置完成\n文件列表: ${Object.keys(files).join( ', ', )}\n当前文件: ${encodeURIComponent( artifact.name, )}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`, ); artifact.url = new_url; } } $.write(allArtifacts, ARTIFACTS_KEY); $.info('上传配置成功'); if (invalid.length > 0) { throw new Error( `同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`, ); } else { $.info(`同步配置成功 ${valid.length} 个`); } } catch (e) { $.error(`同步配置失败,原因:${e.message ?? e}`); throw e; } } async function syncAllArtifacts(_, res) { $.info('开始同步所有远程配置...'); try { await syncArtifacts(); success(res); } catch (e) { $.error(`同步配置失败,原因:${e.message ?? e}`); failed( res, new InternalServerError( `FAILED_TO_SYNC_ARTIFACTS`, `Failed to sync all artifacts`, `Reason: ${e.message ?? e}`, ), ); } } async function syncArtifact(req, res) { let { name } = req.params; $.info(`开始同步远程配置 ${name}...`); const allArtifacts = $.read(ARTIFACTS_KEY); const artifact = findByName(allArtifacts, name); if (!artifact) { $.error(`找不到远程配置 ${name}`); failed( res, new ResourceNotFoundError( 'RESOURCE_NOT_FOUND', `找不到远程配置 ${name}`, ), 404, ); return; } if (!artifact.source) { $.error(`远程配置 ${name} 未设置来源`); failed( res, new ResourceNotFoundError( 'RESOURCE_HAS_NO_SOURCE', `远程配置 ${name} 未设置来源`, ), 404, ); return; } try { const useMihomoExternal = artifact.platform === 'SurgeMac'; if (useMihomoExternal) { $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`); } const output = await produceArtifact({ type: artifact.type, name: artifact.source, platform: artifact.platform, produceOpts: { 'include-unsupported-proxy': artifact.includeUnsupportedProxy, useMihomoExternal, }, }); $.info( `正在上传配置:${artifact.name}\n>>>${JSON.stringify( artifact, null, 2, )}`, ); // if (!output || output.length === 0) // throw new Error('该配置的结果为空 不进行上传'); const resp = await syncToGist({ [encodeURIComponent(artifact.name)]: { content: output, }, }); artifact.updated = new Date().getTime(); const body = JSON.parse(resp.body); delete body.history; delete body.forks; delete body.owner; Object.values(body.files).forEach((file) => { delete file.content; }); $.info('上传配置响应:'); $.info(JSON.stringify(body, null, 2)); let files = body.files; let isGitLab; if (Array.isArray(files)) { isGitLab = true; files = Object.fromEntries(files.map((item) => [item.path, item])); } const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url; const new_url = isGitLab ? raw_url : raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); $.info( `上传配置完成\n文件列表: ${Object.keys(files).join( ', ', )}\n当前文件: ${encodeURIComponent( artifact.name, )}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`, ); artifact.url = new_url; $.write(allArtifacts, ARTIFACTS_KEY); success(res, artifact); } catch (err) { $.error(`远程配置 ${artifact.name} 发生错误: ${err.message ?? err}`); failed( res, new InternalServerError( `FAILED_TO_SYNC_ARTIFACT`, `Failed to sync artifact ${name}`, `Reason: ${err}`, ), ); } } export { produceArtifact, syncArtifacts }; ================================================ FILE: backend/src/restful/token.js ================================================ import { ENV } from '@/vendor/open-api'; import { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants'; import { failed, success } from '@/restful/response'; import $ from '@/core/app'; import { RequestInvalidError, InternalServerError } from '@/restful/errors'; export default function register($app) { if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY); $app.post('/api/token', signToken); $app.route('/api/token/:token').delete(deleteToken); $app.route('/api/tokens').get(getAllTokens); } function deleteToken(req, res) { let { token } = req.params; const { type, name } = req.query; if (!type || !name) return failed( res, new RequestInvalidError( 'INVALID_PAYLOAD', `Payload type and name are required. Please update your front-end(version >= 2.15.76)`, ), ); $.info(`正在删除...\ntoken: ${token}, 类型:${type}, 名称:${name}`); let allTokens = $.read(TOKENS_KEY); allTokens = allTokens.filter( (t) => !(t.token === token && t.type === type && t.name === name), ); $.write(allTokens, TOKENS_KEY); success(res); } function getAllTokens(req, res) { const { type, name } = req.query; const allTokens = $.read(TOKENS_KEY) || []; success( res, type || name ? allTokens.filter( (item) => (type ? item.type === type : true) && (name ? item.name === name : true), ) : allTokens, ); } async function signToken(req, res) { if (!ENV().isNode) { return failed( res, new RequestInvalidError( 'INVALID_ENV', `This endpoint is only available in Node.js environment`, ), ); } try { const { payload, options } = req.body; const ms = eval(`require("ms")`); const type = payload?.type; const name = payload?.name; if (!type || !name) return failed( res, new RequestInvalidError( 'INVALID_PAYLOAD', `payload type and name are required`, ), ); let token = payload?.token; if (token != null) { if (typeof token !== 'string' || token.length < 1) { return failed( res, new RequestInvalidError( 'INVALID_CUSTOM_TOKEN', `Invalid custom token: ${token}`, ), ); } const tokens = $.read(TOKENS_KEY) || []; if ( tokens.find( (t) => t.token === token && t.type === type && t.name === name, ) ) { return failed( res, new RequestInvalidError( 'DUPLICATE_TOKEN', `Token ${token} already exists`, ), ); } } if (type === 'col') { const collections = $.read(COLLECTIONS_KEY) || []; const collection = collections.find((c) => c.name === name); if (!collection) return failed( res, new RequestInvalidError( 'INVALID_COLLECTION', `collection ${name} not found`, ), ); } else if (type === 'file') { const files = $.read(FILES_KEY) || []; const file = files.find((f) => f.name === name); if (!file) return failed( res, new RequestInvalidError( 'INVALID_FILE', `file ${name} not found`, ), ); } else if (type === 'sub') { const subs = $.read(SUBS_KEY) || []; const sub = subs.find((s) => s.name === name); if (!sub) return failed( res, new RequestInvalidError( 'INVALID_SUB', `sub ${name} not found`, ), ); } else { return failed( res, new RequestInvalidError( 'INVALID_TYPE', `type ${name} not supported`, ), ); } let expiresIn = options?.expiresIn; if (options?.expiresIn != null) { expiresIn = ms(options.expiresIn); if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) { return failed( res, new RequestInvalidError( 'INVALID_EXPIRES_IN', `Invalid expiresIn option: ${options.expiresIn}`, ), ); } } // const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH'); const nanoid = eval(`require("nanoid")`); const tokens = $.read(TOKENS_KEY) || []; // const now = Date.now(); // for (const key in tokens) { // const token = tokens[key]; // if (token.exp != null || token.exp < now) { // delete tokens[key]; // } // } if (!token) { do { token = nanoid.customAlphabet(nanoid.urlAlphabet)(); } while ( tokens.find( (t) => t.token === token && t.type === type && t.name === name, ) ); } tokens.push({ ...payload, token, createdAt: Date.now(), expiresIn: expiresIn > 0 ? options?.expiresIn : undefined, exp: expiresIn > 0 ? Date.now() + expiresIn : undefined, }); $.write(tokens, TOKENS_KEY); return success(res, { token, // secret, }); } catch (e) { return failed( res, new InternalServerError( 'TOKEN_SIGN_FAILED', `Failed to sign token`, `Reason: ${e.message ?? e}`, ), ); } } ================================================ FILE: backend/src/test/proxy-parsers/loon.spec.js ================================================ import getLoonParser from '@/core/proxy-utils/parsers/peggy/loon'; import { describe, it } from 'mocha'; import testcases from './testcases'; import { expect } from 'chai'; const parser = getLoonParser(); describe('Loon', function () { describe('shadowsocks', function () { it('test shadowsocks simple', function () { const { input, expected } = testcases.SS.SIMPLE; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); it('test shadowsocks obfs + tls', function () { const { input, expected } = testcases.SS.OBFS_TLS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); it('test shadowsocks obfs + http', function () { const { input, expected } = testcases.SS.OBFS_HTTP; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); }); describe('shadowsocksr', function () { it('test shadowsocksr simple', function () { const { input, expected } = testcases.SSR.SIMPLE; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); }); describe('trojan', function () { it('test trojan simple', function () { const { input, expected } = testcases.TROJAN.SIMPLE; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); it('test trojan + ws', function () { const { input, expected } = testcases.TROJAN.WS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); it('test trojan + wss', function () { const { input, expected } = testcases.TROJAN.WSS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); }); describe('vmess', function () { it('test vmess simple', function () { const { input, expected } = testcases.VMESS.SIMPLE; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vmess + aead', function () { const { input, expected } = testcases.VMESS.AEAD; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vmess + ws', function () { const { input, expected } = testcases.VMESS.WS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vmess + wss', function () { const { input, expected } = testcases.VMESS.WSS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vmess + http', function () { const { input, expected } = testcases.VMESS.HTTP; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vmess + http + tls', function () { const { input, expected } = testcases.VMESS.HTTP_TLS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); }); describe('vless', function () { it('test vless simple', function () { const { input, expected } = testcases.VLESS.SIMPLE; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vless + ws', function () { const { input, expected } = testcases.VLESS.WS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vless + wss', function () { const { input, expected } = testcases.VLESS.WSS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vless + http', function () { const { input, expected } = testcases.VLESS.HTTP; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); it('test vless + http + tls', function () { const { input, expected } = testcases.VLESS.HTTP_TLS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected.Loon); }); }); describe('http(s)', function () { it('test http simple', function () { const { input, expected } = testcases.HTTP.SIMPLE; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); it('test http with authentication', function () { const { input, expected } = testcases.HTTP.AUTH; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); it('test https', function () { const { input, expected } = testcases.HTTP.TLS; const proxy = parser.parse(input.Loon); expect(proxy).eql(expected); }); }); }); ================================================ FILE: backend/src/test/proxy-parsers/qx.spec.js ================================================ import getQXParser from '@/core/proxy-utils/parsers/peggy/qx'; import { describe, it } from 'mocha'; import testcases from './testcases'; import { expect } from 'chai'; const parser = getQXParser(); describe('QX', function () { describe('shadowsocks', function () { it('test shadowsocks simple', function () { const { input, expected } = testcases.SS.SIMPLE; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test shadowsocks obfs + tls', function () { const { input, expected } = testcases.SS.OBFS_TLS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test shadowsocks obfs + http', function () { const { input, expected } = testcases.SS.OBFS_HTTP; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test shadowsocks v2ray-plugin + ws', function () { const { input, expected } = testcases.SS.V2RAY_PLUGIN_WS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test shadowsocks v2ray-plugin + wss', function () { const { input, expected } = testcases.SS.V2RAY_PLUGIN_WSS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); }); describe('shadowsocksr', function () { it('test shadowsocksr simple', function () { const { input, expected } = testcases.SSR.SIMPLE; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); }); describe('trojan', function () { it('test trojan simple', function () { const { input, expected } = testcases.TROJAN.SIMPLE; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test trojan + ws', function () { const { input, expected } = testcases.TROJAN.WS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test trojan + wss', function () { const { input, expected } = testcases.TROJAN.WSS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test trojan + tls fingerprint', function () { const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); }); describe('vmess', function () { it('test vmess simple', function () { const { input, expected } = testcases.VMESS.SIMPLE; const proxy = parser.parse(input.QX); expect(proxy).eql(expected.QX); }); it('test vmess aead', function () { const { input, expected } = testcases.VMESS.AEAD; const proxy = parser.parse(input.QX); expect(proxy).eql(expected.QX); }); it('test vmess + ws', function () { const { input, expected } = testcases.VMESS.WS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected.QX); }); it('test vmess + wss', function () { const { input, expected } = testcases.VMESS.WSS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected.QX); }); it('test vmess + http', function () { const { input, expected } = testcases.VMESS.HTTP; const proxy = parser.parse(input.QX); expect(proxy).eql(expected.QX); }); }); describe('http', function () { it('test http simple', function () { const { input, expected } = testcases.HTTP.SIMPLE; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test http with authentication', function () { const { input, expected } = testcases.HTTP.AUTH; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test https', function () { const { input, expected } = testcases.HTTP.TLS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); }); describe('socks5', function () { it('test socks5 simple', function () { const { input, expected } = testcases.SOCKS5.SIMPLE; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test socks5 with authentication', function () { const { input, expected } = testcases.SOCKS5.AUTH; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); it('test socks5 + tls', function () { const { input, expected } = testcases.SOCKS5.TLS; const proxy = parser.parse(input.QX); expect(proxy).eql(expected); }); }); }); ================================================ FILE: backend/src/test/proxy-parsers/surge.spec.js ================================================ import getSurgeParser from '@/core/proxy-utils/parsers/peggy/surge'; import { describe, it } from 'mocha'; import testcases from './testcases'; import { expect } from 'chai'; const parser = getSurgeParser(); describe('Surge', function () { describe('shadowsocks', function () { it('test shadowsocks simple', function () { const { input, expected } = testcases.SS.SIMPLE; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test shadowsocks obfs + tls', function () { const { input, expected } = testcases.SS.OBFS_TLS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test shadowsocks obfs + http', function () { const { input, expected } = testcases.SS.OBFS_HTTP; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); }); describe('trojan', function () { it('test trojan simple', function () { const { input, expected } = testcases.TROJAN.SIMPLE; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test trojan + ws', function () { const { input, expected } = testcases.TROJAN.WS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test trojan + wss', function () { const { input, expected } = testcases.TROJAN.WSS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test trojan + tls fingerprint', function () { const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); }); describe('vmess', function () { it('test vmess simple', function () { const { input, expected } = testcases.VMESS.SIMPLE; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected.Surge); }); it('test vmess aead', function () { const { input, expected } = testcases.VMESS.AEAD; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected.Surge); }); it('test vmess + ws', function () { const { input, expected } = testcases.VMESS.WS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected.Surge); }); it('test vmess + wss', function () { const { input, expected } = testcases.VMESS.WSS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected.Surge); }); }); describe('http', function () { it('test http simple', function () { const { input, expected } = testcases.HTTP.SIMPLE; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test http with authentication', function () { const { input, expected } = testcases.HTTP.AUTH; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test https', function () { const { input, expected } = testcases.HTTP.TLS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); }); describe('socks5', function () { it('test socks5 simple', function () { const { input, expected } = testcases.SOCKS5.SIMPLE; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test socks5 with authentication', function () { const { input, expected } = testcases.SOCKS5.AUTH; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test socks5 + tls', function () { const { input, expected } = testcases.SOCKS5.TLS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); }); describe('snell', function () { it('test snell simple', function () { const { input, expected } = testcases.SNELL.SIMPLE; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test snell obfs + http', function () { const { input, expected } = testcases.SNELL.OBFS_HTTP; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); it('test snell obfs + tls', function () { const { input, expected } = testcases.SNELL.OBFS_TLS; const proxy = parser.parse(input.Surge); expect(proxy).eql(expected); }); }); }); ================================================ FILE: backend/src/test/proxy-parsers/testcases.js ================================================ function createTestCases() { const name = 'name'; const server = 'example.com'; const port = 10086; const cipher = 'chacha20'; const username = 'username'; const password = 'password'; const obfs_host = 'obfs.com'; const obfs_path = '/resource/file'; const ssr_protocol = 'auth_chain_b'; const ssr_protocol_param = 'def'; const ssr_obfs = 'tls1.2_ticket_fastauth'; const ssr_obfs_param = 'obfs.com'; const uuid = '23ad6b10-8d1a-40f7-8ad0-e3e35cd32291'; const sni = 'sni.com'; const tls_fingerprint = '67:1B:C8:F2:D4:60:DD:A7:EE:60:DA:BB:A3:F9:A4:D7:C8:29:0F:3E:2F:75:B6:A9:46:88:48:7D:D3:97:7E:98'; const SS = { SIMPLE: { input: { Loon: `${name}=shadowsocks,${server},${port},${cipher},"${password}"`, QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},tag=${name}`, Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password}`, }, expected: { type: 'ss', name, server, port, cipher, password, }, }, OBFS_TLS: { input: { Loon: `${name}=shadowsocks,${server},${port},${cipher},"${password}",obfs-name=tls,obfs-uri=${obfs_path},obfs-host=${obfs_host}`, QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password},obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path}`, }, expected: { type: 'ss', name, server, port, cipher, password, plugin: 'obfs', 'plugin-opts': { mode: 'tls', path: obfs_path, host: obfs_host, }, }, }, OBFS_HTTP: { input: { Loon: `${name}=shadowsocks,${server},${port},${cipher},"${password}",obfs-name=http,obfs-uri=${obfs_path},obfs-host=${obfs_host}`, QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path}`, }, expected: { type: 'ss', name, server, port, cipher, password, plugin: 'obfs', 'plugin-opts': { mode: 'http', path: obfs_path, host: obfs_host, }, }, }, V2RAY_PLUGIN_WS: { input: { QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, }, expected: { type: 'ss', name, server, port, cipher, password, plugin: 'v2ray-plugin', 'plugin-opts': { mode: 'websocket', path: obfs_path, host: obfs_host, }, }, }, V2RAY_PLUGIN_WSS: { input: { QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, }, expected: { type: 'ss', name, server, port, cipher, password, plugin: 'v2ray-plugin', 'plugin-opts': { mode: 'websocket', path: obfs_path, host: obfs_host, tls: true, }, }, }, }; const SSR = { SIMPLE: { input: { QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},ssr-protocol=${ssr_protocol},ssr-protocol-param=${ssr_protocol_param},obfs=${ssr_obfs},obfs-host=${ssr_obfs_param},tag=${name}`, Loon: `${name}=shadowsocksr,${server},${port},${cipher},"${password}",protocol=${ssr_protocol},protocol-param=${ssr_protocol_param},obfs=${ssr_obfs},obfs-param=${ssr_obfs_param}`, }, expected: { type: 'ssr', name, server, port, cipher, password, obfs: ssr_obfs, protocol: ssr_protocol, 'obfs-param': ssr_obfs_param, 'protocol-param': ssr_protocol_param, }, }, }; const TROJAN = { SIMPLE: { input: { QX: `trojan=${server}:${port},password=${password},tag=${name}`, Loon: `${name}=trojan,${server},${port},"${password}"`, Surge: `${name}=trojan,${server},${port},password=${password}`, }, expected: { type: 'trojan', name, server, port, password, }, }, WS: { input: { QX: `trojan=${server}:${port},password=${password},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, Loon: `${name}=trojan,${server},${port},"${password}",transport=ws,path=${obfs_path},host=${obfs_host}`, Surge: `${name}=trojan,${server},${port},password=${password},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host}`, }, expected: { type: 'trojan', name, server, port, password, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, }, }, WSS: { input: { QX: `trojan=${server}:${port},password=${password},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tls-verification=false,tls-host=${sni},tag=${name}`, Loon: `${name}=trojan,${server},${port},"${password}",transport=ws,path=${obfs_path},host=${obfs_host},over-tls=true,tls-name=${sni},skip-cert-verify=true`, Surge: `${name}=trojan,${server},${port},password=${password},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host},skip-cert-verify=true,sni=${sni},tls=true`, }, expected: { type: 'trojan', name, server, port, password, network: 'ws', tls: true, 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, 'skip-cert-verify': true, sni, }, }, TLS_FINGERPRINT: { input: { QX: `trojan=${server}:${port},password=${password},tls-verification=false,tls-host=${sni},tls-cert-sha256=${tls_fingerprint},tag=${name},over-tls=true`, Surge: `${name}=trojan,${server},${port},password=${password},skip-cert-verify=true,sni=${sni},tls=true,server-cert-fingerprint-sha256=${tls_fingerprint}`, }, expected: { type: 'trojan', name, server, port, password, tls: true, 'skip-cert-verify': true, sni, 'tls-fingerprint': tls_fingerprint, }, }, }; const VMESS = { SIMPLE: { input: { QX: `vmess=${server}:${port},method=${cipher},password=${uuid},tag=${name}`, Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}"`, Surge: `${name}=vmess,${server},${port},username=${uuid}`, }, expected: { QX: { type: 'vmess', name, server, port, uuid, cipher, alterId: 0, }, Loon: { type: 'vmess', name, server, port, uuid, cipher, alterId: 0, }, Surge: { type: 'vmess', name, server, port, uuid, cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol! alterId: 0, }, }, }, AEAD: { input: { QX: `vmess=${server}:${port},method=${cipher},password=${uuid},aead=true,tag=${name}`, Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",alterId=0`, Surge: `${name}=vmess,${server},${port},username=${uuid},vmess-aead=true`, }, expected: { QX: { type: 'vmess', name, server, port, uuid, cipher, aead: true, alterId: 0, }, Loon: { type: 'vmess', name, server, port, uuid, cipher, alterId: 0, }, Surge: { type: 'vmess', name, server, port, uuid, cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol! alterId: 0, aead: true, }, }, }, WS: { input: { QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path}`, Surge: `${name}=vmess,${server},${port},username=${uuid},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host}`, }, expected: { QX: { type: 'vmess', name, server, port, uuid, cipher, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, alterId: 0, }, Loon: { type: 'vmess', name, server, port, uuid, cipher, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, alterId: 0, }, Surge: { type: 'vmess', name, server, port, uuid, cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol! network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, alterId: 0, }, }, }, WSS: { input: { QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tls-verification=false,tls-host=${sni},tag=${name}`, Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`, Surge: `${name}=vmess,${server},${port},username=${uuid},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host},skip-cert-verify=true,sni=${sni},tls=true`, }, expected: { QX: { type: 'vmess', name, server, port, uuid, cipher, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, tls: true, 'skip-cert-verify': true, sni, alterId: 0, }, Loon: { type: 'vmess', name, server, port, uuid, cipher, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, tls: true, 'skip-cert-verify': true, sni, alterId: 0, }, Surge: { type: 'vmess', name, server, port, uuid, cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol! network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, tls: true, 'skip-cert-verify': true, sni, alterId: 0, }, }, }, HTTP: { input: { QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`, Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path}`, }, expected: { QX: { type: 'vmess', name, server, port, uuid, cipher, network: 'http', 'http-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, alterId: 0, }, Loon: { type: 'vmess', name, server, port, uuid, cipher, network: 'http', 'http-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, alterId: 0, }, }, }, HTTP_TLS: { input: { Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`, }, expected: { Loon: { type: 'vmess', name, server, port, uuid, cipher, network: 'http', 'http-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, tls: true, 'skip-cert-verify': true, sni, alterId: 0, }, }, }, }; const VLESS = { SIMPLE: { input: { Loon: `${name}=vless,${server},${port},"${uuid}"`, }, expected: { Loon: { type: 'vless', name, server, port, uuid, }, }, }, WS: { input: { Loon: `${name}=vless,${server},${port},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path}`, }, expected: { Loon: { type: 'vless', name, server, port, uuid, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, }, }, }, WSS: { input: { Loon: `${name}=vless,${server},${port},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`, }, expected: { Loon: { type: 'vless', name, server, port, uuid, network: 'ws', 'ws-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, tls: true, 'skip-cert-verify': true, sni, }, }, }, HTTP: { input: { Loon: `${name}=vless,${server},${port},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path}`, }, expected: { Loon: { type: 'vless', name, server, port, uuid, network: 'http', 'http-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, }, }, }, HTTP_TLS: { input: { Loon: `${name}=vless,${server},${port},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`, }, expected: { Loon: { type: 'vless', name, server, port, uuid, network: 'http', 'http-opts': { path: obfs_path, headers: { Host: obfs_host, }, }, tls: true, 'skip-cert-verify': true, sni, }, }, }, }; const HTTP = { SIMPLE: { input: { Loon: `${name}=http,${server},${port}`, QX: `http=${server}:${port},tag=${name}`, Surge: `${name}=http,${server},${port}`, }, expected: { type: 'http', name, server, port, }, }, AUTH: { input: { Loon: `${name}=http,${server},${port},${username},"${password}"`, QX: `http=${server}:${port},tag=${name},username=${username},password=${password}`, Surge: `${name}=http,${server},${port},${username},${password}`, }, expected: { type: 'http', name, server, port, username, password, }, }, TLS: { input: { Loon: `${name}=https,${server},${port},${username},"${password}",tls-name=${sni},skip-cert-verify=true`, QX: `http=${server}:${port},username=${username},password=${password},over-tls=true,tls-host=${sni},tls-verification=false,tag=${name}`, Surge: `${name}=https,${server},${port},${username},${password},sni=${sni},skip-cert-verify=true`, }, expected: { type: 'http', name, server, port, username, password, sni, 'skip-cert-verify': true, tls: true, }, }, }; const SOCKS5 = { SIMPLE: { input: { QX: `socks5=${server}:${port},tag=${name}`, Surge: `${name}=socks5,${server},${port}`, }, expected: { type: 'socks5', name, server, port, }, }, AUTH: { input: { QX: `socks5=${server}:${port},tag=${name},username=${username},password=${password}`, Surge: `${name}=socks5,${server},${port},${username},${password}`, }, expected: { type: 'socks5', name, server, port, username, password, }, }, TLS: { input: { QX: `socks5=${server}:${port},username=${username},password=${password},over-tls=true,tls-host=${sni},tls-verification=false,tag=${name}`, Surge: `${name}=socks5-tls,${server},${port},${username},${password},sni=${sni},skip-cert-verify=true`, }, expected: { type: 'socks5', name, server, port, username, password, sni, 'skip-cert-verify': true, tls: true, }, }, }; const SNELL = { SIMPLE: { input: { Surge: `${name}=snell,${server},${port},psk=${password},version=3`, }, expected: { type: 'snell', name, server, port, psk: password, version: 3, }, }, OBFS_HTTP: { input: { Surge: `${name}=snell,${server},${port},psk=${password},version=3,obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path}`, }, expected: { type: 'snell', name, server, port, psk: password, version: 3, 'obfs-opts': { mode: 'http', host: obfs_host, path: obfs_path, }, }, }, OBFS_TLS: { input: { Surge: `${name}=snell,${server},${port},psk=${password},version=3,obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path}`, }, expected: { type: 'snell', name, server, port, psk: password, version: 3, 'obfs-opts': { mode: 'tls', host: obfs_host, path: obfs_path, }, }, }, }; return { SS, SSR, VMESS, VLESS, TROJAN, HTTP, SOCKS5, SNELL, }; } export default createTestCases(); ================================================ FILE: backend/src/utils/database.js ================================================ export function findByName(list, name, field = 'name') { return list.find((item) => item[field] === name); } export function findIndexByName(list, name, field = 'name') { return list.findIndex((item) => item[field] === name); } export function deleteByName(list, name, field = 'name') { const idx = findIndexByName(list, name, field); list.splice(idx, 1); } export function updateByName(list, name, newItem, field = 'name') { const idx = findIndexByName(list, name, field); list[idx] = newItem; } ================================================ FILE: backend/src/utils/dns.js ================================================ import $ from '@/core/app'; import dnsPacket from 'dns-packet'; import { Buffer } from 'buffer'; import { isIPv4 } from '@/utils'; export async function doh({ url, domain, type = 'A', timeout, edns }) { const buf = dnsPacket.encode({ type: 'query', id: 0, flags: dnsPacket.RECURSION_DESIRED, questions: [ { type, name: domain, }, ], additionals: [ { type: 'OPT', name: '.', udpPayloadSize: 4096, flags: 0, options: [ { code: 'CLIENT_SUBNET', ip: edns, sourcePrefixLength: isIPv4(edns) ? 24 : 56, scopePrefixLength: 0, }, ], }, ], }); const b64 = Buffer.from(buf).toString('base64'); const b64url = b64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); const res = await $.http.get({ url: `${url}?dns=${encodeURIComponent(b64url)}`, headers: { Accept: 'application/dns-message', // 'Content-Type': 'application/dns-message', }, // body: buf, 'binary-mode': true, encoding: null, // 使用 null 编码以确保响应是原始二进制数据 timeout, }); return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body)); } ================================================ FILE: backend/src/utils/download.js ================================================ import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants'; import { HTTP, ENV } from '@/vendor/open-api'; import { hex_md5 } from '@/vendor/md5'; import { getPolicyDescriptor } from '@/utils'; import resourceCache from '@/utils/resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache'; import { getFlowField, getFlowHeaders, parseFlowHeaders, validCheck, } from '@/utils/flow'; import $ from '@/core/app'; import { findByName } from '@/utils/database'; import { produceArtifact } from '@/restful/sync'; import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors'; import { ProxyUtils } from '@/core/proxy-utils'; const clashPreprocessor = PROXY_PREPROCESSORS.find( (processor) => processor.name === 'Clash Pre-processor', ); const tasks = new Map(); export default async function download( rawUrl = '', ua, timeout, customProxy, skipCustomCache, awaitCustomCache, noCache, preprocess, ) { let $arguments = {}; let url = rawUrl.replace(/#noFlow$/, ''); const rawArgs = url.split('#'); url = url.split('#')[0]; if (rawArgs.length > 1) { try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` $arguments = JSON.parse(decodeURIComponent(rawArgs[1])); } catch (e) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; $arguments[key] = value == null || value === '' ? true : decodeURIComponent(value); } } } const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV(); const { defaultProxy, defaultUserAgent, defaultTimeout, cacheThreshold: defaultCacheThreshold, } = $.read(SETTINGS_KEY); const cacheThreshold = defaultCacheThreshold || 1024; let proxy = customProxy || defaultProxy; if ($.env.isNode) { proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY'); } const userAgent = ua || defaultUserAgent || 'clash.meta'; let customHeaders; if ($arguments?.headers) { try { const parsed = JSON.parse($arguments?.headers); if ( parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length > 0 ) { const lowerCaseHeaders = { 'user-agent': userAgent }; for (const key in parsed) { lowerCaseHeaders[key.toLowerCase()] = parsed[key]; } customHeaders = lowerCaseHeaders; } } catch (e) { $.error(`解析自定义 headers 失败: ${e}`); } } const requestTimeout = timeout || defaultTimeout || 8000; const id = hex_md5( `${customHeaders ? JSON.stringify(customHeaders) : userAgent}${url}`, ); if ($arguments?.cacheKey === true) { $.error(`使用自定义缓存时 cacheKey 的值不能为空`); $arguments.cacheKey = undefined; } const customCacheKey = $arguments?.cacheKey ? `#sub-store-cached-custom-${$arguments?.cacheKey}` : undefined; if (customCacheKey && !skipCustomCache) { const customCached = $.read(customCacheKey); const cached = resourceCache.get(id); if (!noCache && !$arguments?.noCache && cached) { $.info( `乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`, ); return cached; } if (customCached) { if (awaitCustomCache) { $.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`); try { await download( rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''), ua, timeout, proxy, true, undefined, undefined, preprocess, ); } catch (e) { $.error( `乐观缓存: URL ${url} 更新缓存发生错误 ${ e.message ?? e }`, ); $.info('使用乐观缓存的数据刷新缓存, 防止后续请求'); resourceCache.set(id, customCached); } } else { $.info( `乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`, ); download( rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''), ua, timeout, proxy, true, undefined, undefined, preprocess, ).catch((e) => { $.error( `乐观缓存: URL ${url} 异步更新缓存发生错误 ${ e.message ?? e }`, ); }); } return customCached; } } const downloadUrlMatch = url .split('#')[0] .match(/^\/api\/(file|module)\/(.+)/); if (downloadUrlMatch) { let type = ''; try { type = downloadUrlMatch?.[1]; let name = downloadUrlMatch?.[2]; if (name == null) { throw new Error(`本地 ${type} URL 无效: ${url}`); } name = decodeURIComponent(name); const key = type === 'module' ? MODULES_KEY : FILES_KEY; const item = findByName($.read(key), name); if (!item) { throw new Error(`找不到 ${type}: ${name}`); } if (type === 'module') { return item.content; } else { return await produceArtifact({ type: 'file', name, }); } } catch (err) { $.error( `Error when loading ${type}: ${ url.split('#')[0] }.\n Reason: ${err}`, ); throw new Error(`无法加载 ${type}: ${url}`); } } else if (url?.startsWith('/')) { try { const fs = eval(`require("fs")`); return fs.readFileSync(url.split('#')[0], 'utf8'); } catch (err) { $.error( `Error when reading local file: ${ url.split('#')[0] }.\n Reason: ${err}`, ); throw new Error(`无法从该路径读取文本内容: ${url}`); } } if (!isNode && tasks.has(id)) { return tasks.get(id); } const http = HTTP({ headers: { ...(customHeaders || { 'User-Agent': userAgent }), ...(isStash && proxy ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) } : {}), ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}), }, timeout: requestTimeout, }); let result; // try to find in app cache const cached = resourceCache.get(id); if (!noCache && !$arguments?.noCache && cached) { $.info( `使用缓存: ${url}, ${ customHeaders ? JSON.stringify(customHeaders) : userAgent }`, ); result = cached; if (customCacheKey) { $.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`); $.write(cached, customCacheKey); } } else { const insecure = $arguments?.insecure ? isNode ? { strictSSL: false } : { insecure: true } : undefined; $.info( `Downloading...\n${ customHeaders ? JSON.stringify(customHeaders) : `User-Agent: ${userAgent}` }\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`, ); try { let { body, headers, statusCode } = await http.get({ url, ...(proxy ? { proxy } : {}), ...(isLoon && proxy ? { node: proxy } : {}), ...(isQX && proxy ? { opts: { policy: proxy } } : {}), ...(proxy ? getPolicyDescriptor(proxy) : {}), ...(insecure ? insecure : {}), }); $.info(`statusCode: ${statusCode}`); if (statusCode < 200 || statusCode >= 400) { throw new Error(`statusCode: ${statusCode}`); } if (headers) { const flowInfo = getFlowField(headers); if (flowInfo) { headersResourceCache.set(id, flowInfo); } } if (body.replace(/\s/g, '').length === 0) throw new Error(new Error('远程资源内容为空')); if (preprocess) { try { if (clashPreprocessor.test(body)) { body = clashPreprocessor.parse(body, true); } } catch (e) { $.error(`Clash Pre-processor error: ${e}`); } } let shouldCache = true; if (cacheThreshold) { const size = body.length / 1024; if (size > cacheThreshold) { $.info( `资源大小 ${size.toFixed( 2, )} KB 超过了 ${cacheThreshold} KB, 不缓存`, ); shouldCache = false; } } if (preprocess) { try { const proxies = ProxyUtils.parse(body); if (!Array.isArray(proxies) || proxies.length === 0) { $.error(`URL ${url} 不包含有效节点, 不缓存`); shouldCache = false; } } catch (e) { $.error( `URL ${url} 尝试解析节点失败 ${e.message ?? e}, 不缓存`, ); shouldCache = false; } } if (shouldCache) { resourceCache.set(id, body); if (customCacheKey) { $.info( `URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`, ); $.write(body, customCacheKey); } } result = body; } catch (e) { if (customCacheKey) { const cached = $.read(customCacheKey); if (cached) { $.info( `无法下载 URL ${url}: ${ e.message ?? e }\n使用自定义缓存 ${$arguments?.cacheKey}`, ); return cached; } } throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`); } } // 检查订阅有效性 if ($arguments?.validCheck) { await validCheck( parseFlowHeaders( await getFlowHeaders( url, $arguments.flowUserAgent, undefined, proxy, $arguments.flowUrl, ), ), ); } if (!isNode) { tasks.set(id, result); } return result; } export async function downloadFile(url, file) { const undici = eval("require('undici')"); const fs = eval("require('fs')"); const { pipeline } = eval("require('stream/promises')"); const { Agent, interceptors, request } = undici; $.info(`Downloading file...\nURL: ${url}\nFile: ${file}`); const { body, statusCode } = await request(url, { dispatcher: new Agent().compose( interceptors.redirect({ maxRedirections: 3, throwOnRedirect: true, }), ), }); if (statusCode !== 200) throw new Error(`Failed to download file from ${url}`); const fileStream = fs.createWriteStream(file); await pipeline(body, fileStream); $.info(`File downloaded from ${url} to ${file}`); return file; } ================================================ FILE: backend/src/utils/env.js ================================================ import { version as substoreVersion } from '../../package.json'; import { ENV } from '@/vendor/open-api'; const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket, isLanceX, isEgern, isGUIforCores, } = ENV(); let backend = 'Node'; if (isNode) { backend = 'Node'; } else if (isQX) { backend = 'QX'; } else if (isLoon) { backend = 'Loon'; } else if (isStash) { backend = 'Stash'; } else if (isShadowRocket) { backend = 'Shadowrocket'; } else if (isEgern) { backend = 'Egern'; } else if (isSurge) { backend = 'Surge'; } else if (isLanceX) { backend = 'LanceX'; } else if (isGUIforCores) { backend = 'GUI.for.Cores'; } let meta = {}; let feature = {}; try { if (typeof $environment !== 'undefined') { // eslint-disable-next-line no-undef meta.env = $environment; } if (typeof $loon !== 'undefined') { // eslint-disable-next-line no-undef meta.loon = $loon; } if (typeof $script !== 'undefined') { // eslint-disable-next-line no-undef meta.script = $script; } if (typeof $Plugin !== 'undefined') { // eslint-disable-next-line no-undef meta.plugin = $Plugin; } if (isNode) { meta.node = { version: eval('process.version'), argv: eval('process.argv'), filename: eval('__filename'), dirname: eval('__dirname'), env: {}, }; const env = eval('process.env'); for (const key in env) { if (/^SUB_STORE_/.test(key)) { meta.node.env[key] = env[key]; } } } // eslint-disable-next-line no-empty } catch (e) {} export default { backend, version: substoreVersion, feature, meta, }; ================================================ FILE: backend/src/utils/flow.js ================================================ import { SETTINGS_KEY } from '@/constants'; import { HTTP, ENV } from '@/vendor/open-api'; import { hex_md5 } from '@/vendor/md5'; import { getPolicyDescriptor } from '@/utils'; import $ from '@/core/app'; import headersResourceCache from '@/utils/headers-resource-cache'; export function getFlowField(headers) { const keys = Object.keys(headers); let sub = ''; let webPage = ''; let planName = ''; for (let k of keys) { const lower = k.toLowerCase(); if (lower === 'subscription-userinfo') { sub = headers[k]; } else if (lower === 'profile-web-page-url') { webPage = headers[k]; } else if (lower === 'plan-name') { planName = headers[k]; } } return `${sub || ''}${ webPage ? `; app_url=${encodeURIComponent(webPage)}` : '' }${planName ? `; plan_name=${encodeURIComponent(planName)}` : ''}`; } export async function getFlowHeaders( rawUrl, ua, timeout, customProxy, flowUrl, ) { let url = flowUrl || rawUrl || ''; let $arguments = {}; const rawArgs = url.split('#'); url = url.split('#')[0]; if (rawArgs.length > 1) { try { // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` $arguments = JSON.parse(decodeURIComponent(rawArgs[1])); } catch (e) { for (const pair of rawArgs[1].split('&')) { const key = pair.split('=')[0]; const value = pair.split('=')[1]; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true; $arguments[key] = value == null || value === '' ? true : decodeURIComponent(value); } } } if ($arguments?.noFlow || !/^https?/.test(url)) { return; } const { isStash, isLoon, isShadowRocket, isQX } = ENV(); const insecure = $arguments?.insecure ? $.env.isNode ? { strictSSL: false } : { insecure: true } : undefined; const { defaultProxy, defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY); let proxy = customProxy || defaultProxy; if ($.env.isNode) { proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY'); } const userAgent = ua || defaultFlowUserAgent || 'clash.meta/v1.19.16'; const requestTimeout = timeout || defaultTimeout || 8000; const id = hex_md5(userAgent + url); const cached = headersResourceCache.get(id); let flowInfo; if (!$arguments?.noCache && cached) { $.info(`使用缓存的流量信息: ${url}, ${userAgent}`); flowInfo = cached; } else { const http = HTTP(); if (flowUrl) { let flowUrlHeaders; try { $.info( `使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}`, ); const { headers, body, statusCode } = await http.get({ url: flowUrl, headers: { 'User-Agent': userAgent, }, timeout: requestTimeout, ...(proxy ? { proxy } : {}), ...(isLoon && proxy ? { node: proxy } : {}), ...(isQX && proxy ? { opts: { policy: proxy } } : {}), ...(proxy ? getPolicyDescriptor(proxy) : {}), ...(insecure ? insecure : {}), }); if (statusCode < 200 || statusCode >= 400) { throw new Error(`statusCode: ${statusCode}`); } flowUrlHeaders = headers; const parsed = parseFlowHeaders(body); if ( Number.isFinite(parsed?.total) && Number.isFinite(parsed?.usage?.download) && Number.isFinite(parsed?.usage?.upload) ) { flowInfo = body; } else { throw new Error('响应体中未包含合法的流量信息'); } } catch (e) { $.error( `使用 GET 方法从响应体获取流量信息失败: ${flowUrl}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${ e.message ?? e }`, ); if (flowUrlHeaders) { try { const flowField = getFlowField(flowUrlHeaders); const parsed = parseFlowHeaders(flowField); if ( Number.isFinite(parsed?.total) && Number.isFinite(parsed?.usage?.download) && Number.isFinite(parsed?.usage?.upload) ) { $.info( `使用 GET 方法从响应头获取流量信息成功: ${flowUrl}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}`, ); flowInfo = flowField; } else { throw new Error('响应体中未包含合法的流量信息'); } } catch (e) { $.error( `使用 GET 方法从响应头获取流量信息失败: ${flowUrl}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${ e.message ?? e }`, ); } } } } else { try { $.info( `使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}`, ); const { headers } = await http.head({ url: url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length)[0], headers: { 'User-Agent': userAgent, ...(isStash && proxy ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy), } : {}), ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}), }, timeout: requestTimeout, ...(proxy ? { proxy } : {}), ...(isLoon && proxy ? { node: proxy } : {}), ...(isQX && proxy ? { opts: { policy: proxy } } : {}), ...(proxy ? getPolicyDescriptor(proxy) : {}), ...(insecure ? insecure : {}), }); flowInfo = getFlowField(headers); } catch (e) { $.error( `使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}: ${ e.message ?? e }`, ); } if (!flowInfo) { $.info( `使用 GET 方法获取流量信息: ${url}, User-Agent: ${ userAgent || '' }, Insecure: ${!!insecure}, Proxy: ${proxy}`, ); const { headers } = await http.get({ url: url .split(/[\r\n]+/) .map((i) => i.trim()) .filter((i) => i.length)[0], headers: { 'User-Agent': userAgent, ...(isStash && proxy ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy), } : {}), ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}), }, timeout: requestTimeout, ...(proxy ? { proxy } : {}), ...(isLoon && proxy ? { node: proxy } : {}), ...(isQX && proxy ? { opts: { policy: proxy } } : {}), ...(proxy ? getPolicyDescriptor(proxy) : {}), ...(insecure ? insecure : {}), }); flowInfo = getFlowField(headers); } } if (flowInfo) { flowInfo = flowInfo.trim(); } if (flowInfo) { headersResourceCache.set(id, flowInfo); } } return flowInfo; } export function parseFlowHeaders(flowHeaders) { if (!flowHeaders) return; // unit is KB const uploadMatch = flowHeaders.match( /upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/, ); const upload = uploadMatch == null ? 0 : Number(uploadMatch[1] + uploadMatch[2]); const downloadMatch = flowHeaders.match( /download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/, ); const download = Number(downloadMatch[1] + downloadMatch[2]); const totalMatch = flowHeaders.match( /total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/, ); const total = Number(totalMatch[1] + totalMatch[2]); // optional expire timestamp const expireMatch = flowHeaders.match( /expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/, ); const expires = expireMatch ? Number(expireMatch[1] + expireMatch[2]) : undefined; const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/); const remainingDays = remainingDaysMatch ? Number(remainingDaysMatch[1]) : undefined; const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/); const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined; const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/); const planName = planNameMatch ? decodeURIComponent(planNameMatch[1]) : undefined; return { expires, total, usage: { upload, download }, remainingDays, appUrl, planName, }; } export function flowTransfer(flow, unit = 'B') { const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; let unitIndex = unitList.indexOf(unit); return flow < 1024 || unitIndex === unitList.length - 1 ? { value: (Math.round(flow * 100) / 100).toString(), unit: unit } : flowTransfer(flow / 1024, unitList[++unitIndex]); } export function validCheck(flow) { if (!flow) { throw new Error('没有流量信息'); } if (flow?.expires && flow.expires * 1000 < Date.now()) { const date = new Date(flow.expires * 1000).toLocaleDateString(); throw new Error(`订阅已过期: ${date}`); } if (flow?.total) { const upload = flow.usage?.upload || 0; const download = flow.usage?.download || 0; if (flow.total - upload - download < 0) { const current = upload + download; const currT = flowTransfer(Math.abs(current)); currT.value = current < 0 ? '-' + currT.value : currT.value; const totalT = flowTransfer(flow.total); throw new Error( `流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`, ); } } } export function getRmainingDays(opt = {}) { try { let { resetDay, startDate, cycleDays } = opt; if (['string', 'number'].includes(typeof opt)) { resetDay = opt; } if (startDate && cycleDays) { cycleDays = parseInt(cycleDays); if (isNaN(cycleDays) || cycleDays <= 0) throw new Error('重置周期应为正整数'); if (!startDate || !Date.parse(startDate)) throw new Error('开始日期不合法'); const start = new Date(startDate); const today = new Date(); start.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0); if (start.getTime() > today.getTime()) throw new Error('开始日期应早于现在'); let resetDate = new Date(startDate); resetDate.setDate(resetDate.getDate() + cycleDays); while (resetDate < today) { resetDate.setDate(resetDate.getDate() + cycleDays); } resetDate.setHours(0, 0, 0, 0); const timeDiff = resetDate.getTime() - today.getTime(); const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)); return daysDiff; } else { if (!resetDay) return; resetDay = parseInt(resetDay); if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31) throw new Error('月重置日应为 1-31 之间的整数'); let now = new Date(); let today = now.getDate(); let month = now.getMonth(); let year = now.getFullYear(); let daysInMonth; if (resetDay > today) { daysInMonth = 0; } else { daysInMonth = new Date(year, month + 1, 0).getDate(); } return daysInMonth - today + resetDay; } } catch (e) { $.error(`getRmainingDays failed: ${e.message ?? e}`); } } export function normalizeFlowHeader(flowHeaders, splitHeaders) { try { // 使用 Map 保持顺序并处理重复键 const kvMap = new Map(); flowHeaders .split(';') .map((p) => p.trim()) .filter(Boolean) .forEach((pair) => { const eqIndex = pair.indexOf('='); if (eqIndex === -1) return; const key = pair.slice(0, eqIndex).trim(); const encodedValue = pair.slice(eqIndex + 1).trim(); // 只保留第一个出现的 key if (!kvMap.has(key)) { try { // 解码 URI 组件并保留原始值作为 fallback let decodedValue = decodeURIComponent(encodedValue); if ( [ 'upload', 'download', 'total', 'expire', 'reset_day', ].includes(key) ) { try { decodedValue = Number(decodedValue); if ( ['expire', 'reset_day'].includes(key) && (decodedValue <= 0 || !Number.isFinite(decodedValue)) ) { decodedValue = ''; } else if ( ['upload', 'download', 'total'].includes( key, ) && !Number.isFinite(decodedValue) // 有些机场后端会下发负数 ) { decodedValue = 0; } else { decodedValue = decodedValue.toFixed(0); } } catch (e) { $.error( `Failed to convert value for key "${key}=${encodedValue}": ${ e.message ?? e }`, ); } } kvMap.set(key, decodedValue); } catch (e) { kvMap.set(key, encodedValue); } } }); const subscriptionUserinfo = {}; const headers = { 'subscription-userinfo': '', 'profile-web-page-url': '', 'plan-name': '', }; kvMap.forEach((v, k) => { if (splitHeaders && k === 'app_url') { headers['profile-web-page-url'] = v; } else if (splitHeaders && k === 'plan_name') { headers['plan-name'] = v; } else { subscriptionUserinfo[k] = v; } }); if (Object.keys(subscriptionUserinfo).length > 0) { headers['subscription-userinfo'] = Object.entries( subscriptionUserinfo, ) .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) .join('; '); } return splitHeaders ? headers : headers['subscription-userinfo']; } catch (e) { $.error(`normalizeFlowHeader failed: ${e.message ?? e}`); return splitHeaders ? { 'subscription-userinfo': flowHeaders, } : flowHeaders; } } ================================================ FILE: backend/src/utils/geo.js ================================================ import $ from '@/core/app'; const ISOFlags = { '🏳️‍🌈': ['EXP', 'BAND'], '🇸🇱': ['TEST', 'SOS'], '🇲🇵': ['MP', 'MNP'], '🇸🇴': ['SO', 'SOM'], '🇦🇶': ['AQ', 'ATA'], '🇦🇬': ['AG', 'ATG'], '🇬🇱': ['GL', 'GRL'], '🇿🇼': ['ZW', 'ZWE'], '🇦🇼': ['AW', 'ABW'], '🇲🇱': ['ML', 'MLI'], '🇦🇩': ['AD', 'AND'], '🇦🇪': ['AE', 'ARE'], '🇦🇫': ['AF', 'AFG'], '🇦🇱': ['AL', 'ALB'], '🇦🇲': ['AM', 'ARM'], '🇦🇷': ['AR', 'ARG'], '🇦🇹': ['AT', 'AUT'], '🇦🇺': ['AU', 'AUS'], '🇦🇿': ['AZ', 'AZE'], '🇧🇦': ['BA', 'BIH'], '🇧🇩': ['BD', 'BGD'], '🇧🇪': ['BE', 'BEL'], '🇧🇬': ['BG', 'BGR'], '🇧🇭': ['BH', 'BHR'], '🇧🇴': ['BO', 'BOL'], '🇧🇳': ['BN', 'BRN'], '🇧🇷': ['BR', 'BRA'], '🇧🇹': ['BT', 'BTN'], '🇧🇾': ['BY', 'BLR'], '🇨🇦': ['CA', 'CAN'], '🇨🇭': ['CH', 'CHE'], '🇨🇱': ['CL', 'CHL'], '🇨🇴': ['CO', 'COL'], '🇨🇷': ['CR', 'CRI'], '🇨🇾': ['CY', 'CYP'], '🇨🇿': ['CZ', 'CZE'], '🇩🇪': ['DE', 'DEU'], '🇩🇰': ['DK', 'DNK'], // 新增阿尔及利亚 ISO 代码 '🇩🇿': ['DZ', 'DZA'], '🇪🇨': ['EC', 'ECU'], '🇪🇪': ['EE', 'EST'], '🇪🇬': ['EG', 'EGY'], '🇪🇸': ['ES', 'ESP'], '🇪🇺': ['EU'], '🇫🇮': ['FI', 'FIN'], '🇫🇷': ['FR', 'FRA'], '🇬🇧': ['GB', 'GBR', 'UK'], '🇬🇪': ['GE', 'GEO'], '🇬🇷': ['GR', 'GRC'], '🇬🇹': ['GT', 'GTM'], '🇬🇺': ['GU', 'GUM'], '🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'], '🇭🇷': ['HR', 'HRV'], '🇭🇺': ['HU', 'HUN'], '🇮🇶': ['IQ', 'IRQ'], // 伊拉克 '🇯🇴': ['JO', 'JOR'], '🇯🇵': ['JP', 'JPN', 'TYO'], '🇰🇪': ['KE', 'KEN'], '🇰🇬': ['KG', 'KGZ'], '🇰🇭': ['KH', 'KGZ'], '🇰🇵': ['KP', 'PRK'], '🇰🇷': ['KR', 'KOR', 'SEL'], '🇰🇿': ['KZ', 'KAZ'], '🇮🇩': ['ID', 'IDN'], '🇮🇪': ['IE', 'IRL'], '🇮🇱': ['IL', 'ISR'], '🇮🇲': ['IM', 'IMN'], '🇮🇳': ['IN', 'IND'], '🇮🇷': ['IR', 'IRN'], '🇮🇸': ['IS', 'ISL'], '🇮🇹': ['IT', 'ITA'], '🇱🇦': ['LA', 'LAO'], '🇱🇰': ['LK', 'LKA'], '🇱🇹': ['LT', 'LTU'], '🇱🇺': ['LU', 'LUX'], '🇱🇻': ['LV', 'LVA'], '🇲🇦': ['MA', 'MAR'], '🇲🇩': ['MD', 'MDA'], '🇳🇬': ['NG', 'NGA'], '🇲🇲': ['MM', 'MMR'], '🇲🇰': ['MK', 'MKD'], '🇲🇳': ['MN', 'MNG'], '🇲🇴': ['MO', 'MAC', 'CTM'], '🇲🇹': ['MT', 'MLT'], '🇲🇽': ['MX', 'MEX'], '🇲🇾': ['MY', 'MYS'], '🇳🇱': ['NL', 'NLD', 'AMS'], '🇳🇴': ['NO', 'NOR'], '🇳🇵': ['NP', 'NPL'], '🇳🇿': ['NZ', 'NZL'], '🇴🇲': ['OM', 'OMN'], // 阿曼 '🇵🇦': ['PA', 'PAN'], '🇵🇪': ['PE', 'PER'], '🇵🇭': ['PH', 'PHL'], '🇵🇰': ['PK', 'PAK'], '🇵🇱': ['PL', 'POL'], '🇵🇷': ['PR', 'PRI'], '🇵🇹': ['PT', 'PRT'], '🇵🇾': ['PY', 'PRY'], '🇵🇬': ['PG', 'PNG'], '🇶🇦': ['QA', 'QAT'], '🇷🇴': ['RO', 'ROU'], '🇷🇸': ['RS', 'SRB'], '🇷🇪': ['RE', 'REU'], '🇷🇺': ['RU', 'RUS'], '🇸🇦': ['SA', 'SAU'], '🇼🇸': ['WS', 'WSM'], '🇸🇪': ['SE', 'SWE'], '🇸🇬': ['SG', 'SGP'], '🇸🇮': ['SI', 'SVN'], '🇸🇰': ['SK', 'SVK'], '🇹🇬': ['TG', 'TGO'], // 多哥 '🇹🇭': ['TH', 'THA'], '🇹🇳': ['TN', 'TUN'], '🇹🇷': ['TR', 'TUR'], '🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'], '🇺🇦': ['UA', 'UKR'], '🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'], '🇺🇾': ['UY', 'URY'], // 新增 梵蒂冈 ISO 代码 '🇻🇦': ['VA', 'VAT'], '🇻🇪': ['VE', 'VEN'], '🇻🇳': ['VN', 'VNM'], '🇿🇦': ['ZA', 'ZAF', 'JNB'], '🇨🇳': ['CN', 'CHN', 'BACK'], }; // get proxy flag according to its name export function getFlag(name) { // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js // flags from @surgioproject: https://github.com/surgioproject/surgio/blob/master/lib/misc/flag_cn.ts // refer: https://zh.wikipedia.org/wiki/ISO_3166-1二位字母代码 // refer: https://zh.wikipedia.org/wiki/ISO_3166-1三位字母代码 const Flags = { '🏳️‍🌈': ['流量', '时间', '过期', 'Bandwidth', 'Expire'], '🇸🇱': ['应急', '测试节点'], '🇲🇵': ['北马里亚纳', 'Northern Mariana Islands', 'Saipan', '塞班'], '🇸🇴': ['Somalia', '索马里', '摩加迪沙', 'Mogadishu'], '🇦🇶': ['Antarctica', '南极洲', '南极'], '🇦🇬': ['Antigua and Barbuda', '安提瓜和巴布达'], '🇬🇱': ['Greenland', '格陵兰岛', '格陵兰'], '🇿🇼': ['Zimbabwe', '津巴布韦'], '🇦🇼': ['Aruba', '阿鲁巴'], '🇲🇱': ['Mali', '马里'], '🇦🇩': ['Andorra', '安道尔'], '🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜', 'Dubai'], '🇦🇫': ['Afghanistan', '阿富汗'], '🇦🇱': ['Albania', '阿尔巴尼亚', '阿爾巴尼亞'], '🇦🇲': ['Armenia', '亚美尼亚'], '🇦🇷': ['Argentina', '阿根廷'], '🇦🇹': ['Austria', '奥地利', '奧地利', '维也纳'], '🇼🇸': ['Samoa', '萨摩亚', '薩摩亞'], '🇦🇺': [ 'Australia', '澳大利亚', '澳洲', '墨尔本', '悉尼', '土澳', '京澳', '廣澳', '滬澳', '沪澳', '广澳', 'Sydney', ], '🇦🇿': ['Azerbaijan', '阿塞拜疆'], '🇧🇦': ['Bosnia and Herzegovina', '波黑共和国', '波黑'], '🇧🇩': ['Bangladesh', '孟加拉国', '孟加拉'], '🇧🇪': ['Belgium', '比利时', '比利時'], '🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'], '🇧🇭': ['Bahrain', '巴林'], '🇧🇷': ['Brazil', '巴西', '圣保罗'], '🇧🇳': ['Brunei', '文莱', '汶萊'], '🇧🇾': ['Belarus', '白俄罗斯', '白俄'], '🇧🇴': ['Bolivia', '玻利维亚'], '🇧🇹': ['Bhutan', '不丹', '不丹王国'], '🇨🇦': [ 'Canada', '加拿大', '蒙特利尔', '温哥华', '楓葉', '枫叶', '滑铁卢', '多伦多', 'Waterloo', 'Toronto', ], '🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'], '🇨🇱': ['Chile', '智利'], '🇨🇴': ['Colombia', '哥伦比亚'], '🇨🇷': ['Costa Rica', '哥斯达黎加'], '🇨🇾': ['Cyprus', '塞浦路斯'], // 补充 Czech / Czech Republic 匹配 '🇨🇿': ['Czechia', '捷克', 'Czech', 'Czech Republic'], '🇩🇪': [ 'German', '德国', '德國', '京德', '滬德', '廣德', '沪德', '广德', '法兰克福', 'Frankfurt', '德意志', ], '🇩🇰': ['Denmark', '丹麦', '丹麥'], // 新增 阿尔及利亚 '🇩🇿': ['Algeria', '阿尔及利亚', '阿爾及利亞'], '🇪🇨': ['Ecuador', '厄瓜多尔'], '🇪🇪': ['Estonia', '爱沙尼亚'], '🇪🇬': ['Egypt', '埃及'], '🇪🇸': ['Spain', '西班牙'], '🇪🇺': ['European Union', '欧盟', '欧罗巴'], '🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'], '🇫🇷': ['France', '法国', '法國', '巴黎'], '🇬🇧': [ 'Great Britain', '英国', 'England', 'United Kingdom', '伦敦', '英', 'London', ], '🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'], '🇬🇷': ['Greece', '希腊', '希臘'], '🇬🇺': ['Guam', '关岛', '關島'], '🇬🇹': ['Guatemala', '危地马拉'], '🇭🇰': [ 'Hongkong', '香港', 'Hong Kong', 'HongKong', 'HONG KONG', '深港', '沪港', '呼港', '穗港', '京港', '港', ], '🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'], '🇭🇺': ['Hungary', '匈牙利'], '🇮🇶': ['Iraq', '伊拉克', '巴格达', 'Baghdad'], // 伊拉克 '🇯🇴': ['Jordan', '约旦'], '🇯🇵': [ 'Japan', '日本', '东京', '大阪', '埼玉', '沪日', '穗日', '川日', '中日', '泉日', '杭日', '深日', '辽日', '广日', '大坂', 'Osaka', 'Tokyo', ], '🇰🇪': ['Kenya', '肯尼亚'], '🇰🇬': ['Kyrgyzstan', '吉尔吉斯斯坦'], '🇰🇭': ['Cambodia', '柬埔寨'], '🇰🇵': ['North Korea', '朝鲜'], '🇰🇷': [ 'Korea', '韩国', '韓國', '韩', '韓', '首尔', '春川', 'Chuncheon', 'Seoul', ], '🇰🇿': ['Kazakhstan', '哈萨克斯坦', '哈萨克'], '🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'], '🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'], '🇮🇱': ['Israel', '以色列'], '🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'], '🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'], '🇮🇷': ['Iran', '伊朗'], '🇮🇸': ['Iceland', '冰岛', '冰島'], '🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'], '🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'], '🇱🇦': ['Laos', '老挝', '老撾'], '🇱🇹': ['Lithuania', '立陶宛'], '🇱🇺': ['Luxembourg', '卢森堡'], '🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'], '🇲🇦': ['Morocco', '摩洛哥'], '🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'], '🇲🇲': ['Myanmar', '缅甸', '緬甸'], '🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'], '🇲🇰': ['Macedonia', '马其顿', '馬其頓'], '🇲🇳': ['Mongolia', '蒙古'], '🇲🇴': ['Macao', '澳门', '澳門', 'CTM'], '🇲🇹': ['Malta', '马耳他'], '🇲🇽': ['Mexico', '墨西哥'], '🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'], '🇳🇱': [ 'Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹', 'Amsterdam', ], '🇳🇴': ['Norway', '挪威'], '🇳🇵': ['Nepal', '尼泊尔'], '🇳🇿': ['New Zealand', '新西兰', '新西蘭'], '🇴🇲': ['Oman', '阿曼', '马斯喀特'], '🇵🇦': ['Panama', '巴拿马'], '🇵🇪': ['Peru', '秘鲁', '祕魯'], '🇵🇭': ['Philippines', '菲律宾', '菲律賓'], '🇵🇰': ['Pakistan', '巴基斯坦'], '🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'], '🇵🇷': ['Puerto Rico', '波多黎各'], '🇵🇹': ['Portugal', '葡萄牙'], '🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'], '🇵🇾': ['Paraguay', '巴拉圭'], '🇶🇦': ['Qatar', '卡塔尔', '卡塔爾'], '🇷🇴': ['Romania', '罗马尼亚'], '🇷🇸': ['Serbia', '塞尔维亚'], '🇷🇪': ['Réunion', '留尼汪', '法属留尼汪'], '🇷🇺': [ 'Russia', '俄罗斯', '俄国', '俄羅斯', '伯力', '莫斯科', '圣彼得堡', '西伯利亚', '京俄', '杭俄', '廣俄', '滬俄', '广俄', '沪俄', 'Moscow', ], '🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'], '🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'], '🇸🇬': [ 'Singapore', '新加坡', '狮城', '沪新', '京新', '中新', '泉新', '穗新', '深新', '杭新', '广新', '廣新', '滬新', ], '🇸🇮': ['Slovenia', '斯洛文尼亚'], '🇸🇰': ['Slovakia', '斯洛伐克'], '🇹🇬': ['Togo', '多哥', '洛美', 'Lomé', 'Lome'], // 多哥 '🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'], '🇹🇳': ['Tunisia', '突尼斯'], '🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'], '🇹🇼': [ 'Taiwan', '台湾', '臺灣', '台灣', '中華民國', '中华民国', '台北', '台中', '新北', '彰化', '台', '臺', 'Taipei', 'Tai Wan', ], '🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'], '🇺🇸': [ 'United States', '美国', 'America', '美', '京美', '波特兰', '达拉斯', '俄勒冈', 'Oregon', '凤凰城', '费利蒙', '硅谷', '矽谷', '拉斯维加斯', '洛杉矶', '圣何塞', '圣克拉拉', '西雅图', '芝加哥', '沪美', '哥伦布', '纽约', 'New York', 'Los Angeles', 'San Jose', 'Sillicon Valley', 'Michigan', '俄亥俄', 'Ohio', '马纳萨斯', 'Manassas', '弗吉尼亚', 'Virginia', ], '🇺🇾': ['Uruguay', '乌拉圭'], // 新增 梵蒂冈 及别名 '🇻🇦': ['Vatican', 'Vatican City', 'Holy See', '梵蒂冈', '梵蒂岡'], '🇻🇪': ['Venezuela', '委内瑞拉'], '🇻🇳': ['Vietnam', '越南', '胡志明'], '🇿🇦': ['South Africa', '南非'], '🇨🇳': [ 'China', '中国', '中國', '回国', '回國', '国内', '國內', '华东', '华西', '华南', '华北', '华中', '江苏', '北京', '上海', '广州', '深圳', '杭州', '徐州', '青岛', '宁波', '镇江', ], }; // 原旗帜或空 let Flag = name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] || '🏴‍☠️'; //console.log(`oldFlag = ${Flag}`) // 旗帜匹配 for (let flag of Object.keys(Flags)) { const keywords = Flags[flag]; //console.log(`keywords = ${keywords}`) if ( // 不精确匹配(只要包含就算,忽略大小写) keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name)) ) { if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) { return (Flag = '🇨🇳'); } return (Flag = flag); } } // ISO旗帜匹配 for (let flag of Object.keys(ISOFlags)) { const keywords = ISOFlags[flag]; //console.log(`keywords = ${keywords}`) if ( // 精确匹配(两侧均有分割) keywords.some((keyword) => RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name), ) ) { const isCN2 = flag == '🇨🇳' && RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name); if (!isCN2) { return (Flag = flag); } } } //console.log(`Final Flag = ${Flag}`) return Flag; } export function getISO(name) { return ISOFlags[getFlag(name)]?.[0]; } // remove flag export function removeFlag(str) { return str .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '') .trim(); } export class MMDB { constructor({ country, asn } = {}) { if ($.env.isNode) { const Reader = eval(`require("@maxmind/geoip2-node")`).Reader; const fs = eval("require('fs')"); const countryFile = country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH'); const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH'); // $.info( // `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync( // countryFile, // )}`, // ); if (countryFile) { this.countryReader = Reader.openBuffer( fs.readFileSync(countryFile), ); } // $.info( // `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync( // asnFile, // )}`, // ); if (asnFile) { if (!fs.existsSync(asnFile)) throw new Error('GeoLite2 ASN MMDB does not exist'); this.asnReader = Reader.openBuffer(fs.readFileSync(asnFile)); } } } geoip(ip) { return this.countryReader?.country(ip)?.country?.isoCode; } ipaso(ip) { return this.asnReader?.asn(ip)?.autonomousSystemOrganization; } ipasn(ip) { return this.asnReader?.asn(ip)?.autonomousSystemNumber; } } ================================================ FILE: backend/src/utils/gist.js ================================================ import { HTTP, ENV } from '@/vendor/open-api'; import { getPolicyDescriptor } from '@/utils'; import $ from '@/core/app'; import { SETTINGS_KEY } from '@/constants'; /** * Gist backup */ export default class Gist { constructor({ token, key, syncPlatform }) { const { isStash, isLoon, isShadowRocket, isQX } = ENV(); const { defaultProxy, defaultTimeout: timeout, githubProxy, } = $.read(SETTINGS_KEY); let proxy = defaultProxy; if ($.env.isNode) { proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY'); } if (syncPlatform === 'gitlab') { this.headers = { 'PRIVATE-TOKEN': `${token}`, 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', }; this.http = HTTP({ baseURL: 'https://gitlab.com/api/v4', headers: { ...this.headers, ...(isStash && proxy ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy), } : {}), ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}), }, ...(proxy ? { proxy } : {}), ...(isLoon && proxy ? { node: proxy } : {}), ...(isQX && proxy ? { opts: { policy: proxy } } : {}), ...(proxy ? getPolicyDescriptor(proxy) : {}), timeout: timeout || 8000, events: { onResponse: (resp) => { if (/^[45]/.test(String(resp.statusCode))) { const body = JSON.parse(resp.body); return Promise.reject( `ERROR: ${body.message?.error ?? body.message}`, ); } else { return resp; } }, }, }); } else { this.headers = { Authorization: `token ${token}`, 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36', }; this.http = HTTP({ baseURL: `${ githubProxy ? `${githubProxy}/` : '' }https://api.github.com`, headers: { ...this.headers, ...(isStash && proxy ? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy), } : {}), ...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}), }, ...(proxy ? { proxy } : {}), ...(isLoon && proxy ? { node: proxy } : {}), ...(isQX && proxy ? { opts: { policy: proxy } } : {}), ...(proxy ? getPolicyDescriptor(proxy) : {}), timeout: timeout || 8000, events: { onResponse: (resp) => { if (/^[45]/.test(String(resp.statusCode))) { return Promise.reject( `ERROR: ${JSON.parse(resp.body).message}`, ); } else { return resp; } }, }, }); } this.key = key; this.syncPlatform = syncPlatform; } async locate() { if (this.syncPlatform === 'gitlab') { return this.http.get('/snippets').then((response) => { const gists = JSON.parse(response.body); for (let g of gists) { if (g.title === this.key) { return g; } } return; }); } else { return this.http .get('/gists?per_page=100&page=1') .then((response) => { const gists = JSON.parse(response.body); $.info(`获取到当前 GitHub 用户的 gist: ${gists.length} 个`); for (let g of gists) { if (g.description === this.key) { return g; } } return; }); } } async upload(input) { if (Object.keys(input).length === 0) { return Promise.reject('未提供需上传的文件'); } const gist = await this.locate(); let files = input; if (gist?.id) { if (this.syncPlatform === 'gitlab') { gist.files = gist.files.reduce((acc, item) => { acc[item.path] = item; return acc; }, {}); } // console.log(`files`, files); // console.log(`gist`, gist.files); let actions = []; const result = { ...gist.files }; Object.keys(files).map((key) => { if (result[key]) { if ( files[key].content == null || files[key].content === '' ) { delete result[key]; actions.push({ action: 'delete', file_path: key, }); } else { result[key] = files[key]; actions.push({ action: 'update', file_path: key, content: files[key].content, }); } } else { if ( files[key].content == null || files[key].content === '' ) { delete result[key]; delete files[key]; } else { result[key] = files[key]; actions.push({ action: 'create', file_path: key, content: files[key].content, }); } } }); // console.log(`result`, result); // console.log(`files`, files); // console.log(`actions`, actions); if (this.syncPlatform === 'gitlab') { if (Object.keys(result).length === 0) { return Promise.reject( '本次操作将导致所有文件的内容都为空, 无法更新 snippet', ); } if (Object.keys(result).length > 10) { return Promise.reject( '本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet', ); } files = actions; return this.http.put({ headers: { ...this.headers, 'Content-Type': 'application/json', }, url: `/snippets/${gist.id}`, body: JSON.stringify({ files }), }); } else { if (Object.keys(result).length === 0) { return Promise.reject( '本次操作将导致所有文件的内容都为空, 无法更新 gist', ); } return this.http.patch({ url: `/gists/${gist.id}`, body: JSON.stringify({ files }), }); } } else { files = Object.entries(files).reduce((acc, [key, file]) => { if (file.content !== null && file.content !== '') { acc[key] = file; } return acc; }, {}); if (this.syncPlatform === 'gitlab') { if (Object.keys(files).length === 0) { return Promise.reject( '所有文件的内容都为空, 无法创建 snippet', ); } files = Object.keys(files).map((key) => ({ file_path: key, content: files[key].content, })); return this.http.post({ headers: { ...this.headers, 'Content-Type': 'application/json', }, url: '/snippets', body: JSON.stringify({ title: this.key, visibility: 'private', files, }), }); } else { if (Object.keys(files).length === 0) { return Promise.reject( '所有文件的内容都为空, 无法创建 gist', ); } return this.http.post({ url: '/gists', body: JSON.stringify({ description: this.key, public: false, files, }), }); } } } async download(filename) { const gist = await this.locate(); if (gist?.id) { try { const { files } = await this.http .get(`/gists/${gist.id}`) .then((resp) => JSON.parse(resp.body)); const url = files[filename].raw_url; return await this.http.get(url).then((resp) => resp.body); } catch (err) { return Promise.reject(err); } } else { return Promise.reject(`找不到 Sub-Store Gist (${this.key})`); } } } ================================================ FILE: backend/src/utils/headers-resource-cache.js ================================================ import $ from '@/core/app'; import { HEADERS_RESOURCE_CACHE_KEY, DEFAULT_HEADERS_CACHE_TTL, SETTINGS_KEY, } from '@/constants'; class ResourceCache { constructor() { if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) { $.write('{}', HEADERS_RESOURCE_CACHE_KEY); } try { this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY)); } catch (e) { $.error( `解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${ e?.message ?? e }`, ); this.resourceCache = {}; $.write('{}', HEADERS_RESOURCE_CACHE_KEY); } this._cleanup(); } _cleanup(prefix, ttl) { const resolvedTTL = normalizeTTL(ttl) ?? 0; let clear = false; const now = Date.now(); Object.entries(this.resourceCache).forEach((entry) => { const [id, cached] = entry; const shouldDelete = !cached.time || cached.time < now + resolvedTTL; if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) { delete this.resourceCache[id]; clear = true; } }); if (clear) this._persist(); } revokeAll() { this.resourceCache = {}; this._persist(); } _persist() { $.write(JSON.stringify(this.resourceCache), HEADERS_RESOURCE_CACHE_KEY); } gettime(id) { const time = this.resourceCache[id] && this.resourceCache[id].time; if (time && new Date().getTime() <= time) { return this.resourceCache[id].time; } return null; } get(id, ttl, remove) { const resolvedTTL = normalizeTTL(ttl) ?? 0; const cached = this.resourceCache[id]; const time = cached && cached.time; if (time) { if (Date.now() + resolvedTTL <= time) return cached.data; if (remove) { delete this.resourceCache[id]; this._persist(); } } return null; } set(id, value, ttl) { const resolvedTTL = normalizeTTL(ttl) ?? getTTL(); this.resourceCache[id] = { time: Date.now() + resolvedTTL, data: value, }; this._persist(); } } function normalizeTTL(ttl) { const value = Number(ttl); if (!isFinite(value)) return null; if (value > 0) return value; return null; } function getTTL() { const settings = $.read(SETTINGS_KEY); let ttl = settings?.headersCacheTtl; if (ttl) { ttl = Number(ttl); if (isFinite(ttl) && ttl > 0) { return ttl * 1000; } } return DEFAULT_HEADERS_CACHE_TTL; } export default new ResourceCache(); ================================================ FILE: backend/src/utils/index.js ================================================ import * as ipAddress from 'ip-address'; // source: https://stackoverflow.com/a/36760050 const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/; // source: https://ihateregex.io/expr/ipv6/ const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; function isIPv4(ip) { return IPV4_REGEX.test(ip); } function isIPv6(ip) { return IPV6_REGEX.test(ip); } function isValidPortNumber(port) { return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test( port, ); } function isNotBlank(str) { return typeof str === 'string' && str.trim().length > 0; } function getIfNotBlank(str, defaultValue) { return isNotBlank(str) ? str : defaultValue; } function isPresent(obj) { return typeof obj !== 'undefined' && obj !== null; } function getIfPresent(obj, defaultValue) { return isPresent(obj) ? obj : defaultValue; } function getPolicyDescriptor(str) { if (!str) return {}; return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str) ? { 'policy-descriptor': str, } : { policy: str, }; } // const utf8ArrayToStr = // typeof TextDecoder !== 'undefined' // ? (v) => new TextDecoder().decode(new Uint8Array(v)) // : (function () { // var charCache = new Array(128); // Preallocate the cache for the common single byte chars // var charFromCodePt = String.fromCodePoint || String.fromCharCode; // var result = []; // return function (array) { // var codePt, byte1; // var buffLen = array.length; // result.length = 0; // for (var i = 0; i < buffLen; ) { // byte1 = array[i++]; // if (byte1 <= 0x7f) { // codePt = byte1; // } else if (byte1 <= 0xdf) { // codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f); // } else if (byte1 <= 0xef) { // codePt = // ((byte1 & 0x0f) << 12) | // ((array[i++] & 0x3f) << 6) | // (array[i++] & 0x3f); // } else if (String.fromCodePoint) { // codePt = // ((byte1 & 0x07) << 18) | // ((array[i++] & 0x3f) << 12) | // ((array[i++] & 0x3f) << 6) | // (array[i++] & 0x3f); // } else { // codePt = 63; // Cannot convert four byte code points, so use "?" instead // i += 3; // } // result.push( // charCache[codePt] || // (charCache[codePt] = charFromCodePt(codePt)), // ); // } // return result.join(''); // }; // })(); function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } function getRandomPort(portString) { let portParts = portString.split(/,|\//); let randomPart = portParts[Math.floor(Math.random() * portParts.length)]; if (randomPart.includes('-')) { let [min, max] = randomPart.split('-').map(Number); return getRandomInt(min, max); } else { return Number(randomPart); } } function numberToString(value) { return Number.isSafeInteger(value) ? String(value) : BigInt(value).toString(); } function isValidUUID(uuid) { return ( typeof uuid === 'string' && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( uuid, ) ); } function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') { const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) { return ''; } const pad = (num) => String(num).padStart(2, '0'); const replacements = { YYYY: d.getFullYear(), MM: pad(d.getMonth() + 1), DD: pad(d.getDate()), HH: pad(d.getHours()), mm: pad(d.getMinutes()), ss: pad(d.getSeconds()), }; return format.replace( /YYYY|MM|DD|HH|mm|ss/g, (match) => replacements[match], ); } function isPlainObject(obj) { return ( obj !== null && typeof obj === 'object' && [null, Object.prototype].includes(Object.getPrototypeOf(obj)) ); } export { isPlainObject, formatDateTime, isValidUUID, ipAddress, isIPv4, isIPv6, isValidPortNumber, isNotBlank, getIfNotBlank, isPresent, getIfPresent, // utf8ArrayToStr, getPolicyDescriptor, getRandomPort, numberToString, }; ================================================ FILE: backend/src/utils/logical.js ================================================ function AND(...args) { return args.reduce((a, b) => a.map((c, i) => b[i] && c)); } function OR(...args) { return args.reduce((a, b) => a.map((c, i) => b[i] || c)); } function NOT(array) { return array.map((c) => !c); } function FULL(length, bool) { return [...Array(length).keys()].map(() => bool); } export { AND, OR, NOT, FULL }; ================================================ FILE: backend/src/utils/migration.js ================================================ import { SUBS_KEY, COLLECTIONS_KEY, SCHEMA_VERSION_KEY, ARTIFACTS_KEY, RULES_KEY, FILES_KEY, TOKENS_KEY, } from '@/constants'; import $ from '@/core/app'; export default function migrate() { migrateV2(); } function migrateV2() { const version = $.read(SCHEMA_VERSION_KEY); if (!version) doMigrationV2(); // write the current version if (version !== '2.0') { $.write('2.0', SCHEMA_VERSION_KEY); } } function doMigrationV2() { $.info('Start migrating...'); // 1. migrate subscriptions const subs = $.read(SUBS_KEY) || {}; const newSubs = Object.values(subs).map((sub) => { // set default source to remote sub.source = sub.source || 'remote'; migrateDisplayName(sub); migrateProcesses(sub); return sub; }); $.write(newSubs, SUBS_KEY); // 2. migrate collections const collections = $.read(COLLECTIONS_KEY) || {}; const newCollections = Object.values(collections).map((collection) => { delete collection.ua; migrateDisplayName(collection); migrateProcesses(collection); return collection; }); $.write(newCollections, COLLECTIONS_KEY); // 3. migrate artifacts const artifacts = $.read(ARTIFACTS_KEY) || {}; const newArtifacts = Object.values(artifacts); $.write(newArtifacts, ARTIFACTS_KEY); // 4. migrate rules const rules = $.read(RULES_KEY) || {}; const newRules = Object.values(rules); $.write(newRules, RULES_KEY); // 5. migrate files const files = $.read(FILES_KEY) || {}; const newFiles = Object.values(files); $.write(newFiles, FILES_KEY); // 6. migrate tokens const tokens = $.read(TOKENS_KEY) || {}; const newTokens = Object.values(tokens); $.write(newTokens, TOKENS_KEY); // 7. delete builtin rules delete $.cache.builtin; $.info('Migration complete!'); function migrateDisplayName(item) { const displayName = item['display-name']; if (displayName) { item.displayName = displayName; delete item['display-name']; } } function migrateProcesses(item) { const processes = item.process; if (!processes || processes.length === 0) return; const newProcesses = []; const quickSettingOperator = { type: 'Quick Setting Operator', args: { udp: 'DEFAULT', tfo: 'DEFAULT', scert: 'DEFAULT', 'vmess aead': 'DEFAULT', useless: 'DEFAULT', }, }; for (const p of processes) { if (!p.type) continue; if (p.type === 'Useless Filter') { quickSettingOperator.args.useless = 'ENABLED'; } else if (p.type === 'Set Property Operator') { const { key, value } = p.args; switch (key) { case 'udp': quickSettingOperator.args.udp = value ? 'ENABLED' : 'DISABLED'; break; case 'tfo': quickSettingOperator.args.tfo = value ? 'ENABLED' : 'DISABLED'; break; case 'skip-cert-verify': quickSettingOperator.args.scert = value ? 'ENABLED' : 'DISABLED'; break; case 'aead': quickSettingOperator.args['vmess aead'] = value ? 'ENABLED' : 'DISABLED'; break; } } else if (p.type.indexOf('Keyword') !== -1) { // drop keyword operators and keyword filters } else if (p.type === 'Flag Operator') { // set default args const add = typeof p.args === 'undefined' ? true : p.args; p.args = { mode: add ? 'add' : 'remove', }; newProcesses.push(p); } else { newProcesses.push(p); } } newProcesses.unshift(quickSettingOperator); item.process = newProcesses; } } ================================================ FILE: backend/src/utils/resource-cache.js ================================================ import $ from '@/core/app'; import { RESOURCE_CACHE_KEY, DEFAULT_CACHE_TTL, SETTINGS_KEY, } from '@/constants'; class ResourceCache { constructor() { if (!$.read(RESOURCE_CACHE_KEY)) { $.write('{}', RESOURCE_CACHE_KEY); } try { this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY)); } catch (e) { $.error( `解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${ e?.message ?? e }`, ); this.resourceCache = {}; $.write('{}', RESOURCE_CACHE_KEY); } this._cleanup(); } _cleanup(prefix, ttl) { const resolvedTTL = normalizeTTL(ttl) ?? 0; let clear = false; const now = Date.now(); Object.entries(this.resourceCache).forEach((entry) => { const [id, cached] = entry; const shouldDelete = !cached.time || cached.time < now + resolvedTTL; if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) { delete this.resourceCache[id]; clear = true; } }); if (clear) this._persist(); } revokeAll() { this.resourceCache = {}; this._persist(); } _persist() { $.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY); } gettime(id) { const time = this.resourceCache[id] && this.resourceCache[id].time; if (time && new Date().getTime() <= time) { return this.resourceCache[id].time; } return null; } get(id, ttl, remove) { const resolvedTTL = normalizeTTL(ttl) ?? 0; const cached = this.resourceCache[id]; const time = cached && cached.time; if (time) { if (Date.now() + resolvedTTL <= time) return cached.data; if (remove) { delete this.resourceCache[id]; this._persist(); } } return null; } set(id, value, ttl) { const resolvedTTL = normalizeTTL(ttl) ?? getTTL(); this.resourceCache[id] = { time: Date.now() + resolvedTTL, data: value, }; this._persist(); } } function normalizeTTL(ttl) { const value = Number(ttl); if (!isFinite(value)) return null; if (value > 0) return value; return null; } function getTTL() { const settings = $.read(SETTINGS_KEY); let ttl = settings?.resourceCacheTtl; if (ttl) { ttl = Number(ttl); if (isFinite(ttl) && ttl > 0) { return ttl * 1000; } } return DEFAULT_CACHE_TTL; } export default new ResourceCache(); ================================================ FILE: backend/src/utils/rs.js ================================================ import rs from 'jsrsasign'; export function generateFingerprint(caStr) { const hex = rs.pemtohex(caStr); const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256'); return fingerPrint.match(/.{2}/g).join(':').toUpperCase(); } export default { generateFingerprint, }; ================================================ FILE: backend/src/utils/script-resource-cache.js ================================================ import $ from '@/core/app'; import { SCRIPT_RESOURCE_CACHE_KEY, DEFAULT_SCRIPT_CACHE_TTL, SETTINGS_KEY, } from '@/constants'; class ResourceCache { constructor() { if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) { $.write('{}', SCRIPT_RESOURCE_CACHE_KEY); } try { this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY)); } catch (e) { $.error( `解析持久化缓存中的 ${SCRIPT_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${ e?.message ?? e }`, ); this.resourceCache = {}; $.write('{}', SCRIPT_RESOURCE_CACHE_KEY); } this._cleanup(); } _cleanup(prefix, ttl) { const resolvedTTL = normalizeTTL(ttl) ?? 0; let clear = false; const now = Date.now(); Object.entries(this.resourceCache).forEach((entry) => { const [id, cached] = entry; const shouldDelete = !cached.time || cached.time < now + resolvedTTL; if (shouldDelete && (prefix ? id.startsWith(prefix) : true)) { delete this.resourceCache[id]; clear = true; } }); if (clear) this._persist(); } revokeAll() { this.resourceCache = {}; this._persist(); } _persist() { $.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY); } gettime(id) { const time = this.resourceCache[id] && this.resourceCache[id].time; if (time && new Date().getTime() <= time) { return this.resourceCache[id].time; } return null; } get(id, ttl, remove) { const resolvedTTL = normalizeTTL(ttl) ?? 0; const cached = this.resourceCache[id]; const time = cached && cached.time; if (time) { if (Date.now() + resolvedTTL <= time) return cached.data; if (remove) { delete this.resourceCache[id]; this._persist(); } } return null; } set(id, value, ttl) { const resolvedTTL = normalizeTTL(ttl) ?? getTTL(); this.resourceCache[id] = { time: Date.now() + resolvedTTL, data: value, }; this._persist(); } } function normalizeTTL(ttl) { const value = Number(ttl); if (!isFinite(value)) return null; if (value > 0) return value; return null; } function getTTL() { const settings = $.read(SETTINGS_KEY); let ttl = settings?.scriptCacheTtl; if (ttl) { ttl = Number(ttl); if (isFinite(ttl) && ttl > 0) { return ttl * 1000; } } return DEFAULT_SCRIPT_CACHE_TTL; } export default new ResourceCache(); ================================================ FILE: backend/src/utils/user-agent.js ================================================ import gte from 'semver/functions/gte'; import coerce from 'semver/functions/coerce'; import $ from '@/core/app'; export function getUserAgentFromHeaders(headers) { const keys = Object.keys(headers); let UA = ''; let ua = ''; let accept = ''; for (let k of keys) { const lower = k.toLowerCase(); if (lower === 'user-agent') { UA = headers[k]; ua = UA.toLowerCase(); } else if (lower === 'accept') { accept = headers[k]; } } return { UA, ua, accept }; } export function getPlatformFromUserAgent({ ua, UA, accept }) { if (UA.indexOf('Quantumult%20X') !== -1) { return 'QX'; } else if (ua.indexOf('egern') !== -1) { return 'Egern'; } else if (UA.indexOf('Surfboard') !== -1) { return 'Surfboard'; } else if (UA.indexOf('Surge Mac') !== -1) { return 'SurgeMac'; } else if (UA.indexOf('Surge') !== -1) { return 'Surge'; } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) { return 'Loon'; } else if (UA.indexOf('Shadowrocket') !== -1) { return 'Shadowrocket'; } else if (UA.indexOf('Stash') !== -1) { return 'Stash'; } else if ( ua === 'meta' || (ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) || ua.indexOf('clash-verge') !== -1 || ua.indexOf('flclash') !== -1 ) { return 'ClashMeta'; } else if (ua.indexOf('clash') !== -1) { return 'Clash'; } else if (ua.indexOf('v2ray') !== -1) { return 'V2Ray'; } else if (ua.indexOf('sing-box') !== -1 || ua.indexOf('singbox') !== -1) { return 'sing-box'; } else if (accept.indexOf('application/json') === 0) { return 'JSON'; } else { return 'V2Ray'; } } export function getPlatformFromHeaders(headers) { const { UA, ua, accept } = getUserAgentFromHeaders(headers); return getPlatformFromUserAgent({ ua, UA, accept }); } export function shouldIncludeUnsupportedProxy(platform, headers) { try { const { UA, ua, accept } = getUserAgentFromHeaders(headers); const target = getPlatformFromUserAgent({ UA, ua, accept }); const coerceVersion = coerce(ua); const { major } = coerceVersion; if ( (['SurgeMac', 'Surge'].includes(platform) && target === 'SurgeMac' && major >= 9860) || (platform === 'Surge' && target === 'Surge' && major >= 3613) ) { return true; } // if ( // platform === 'Egern' && // target === 'Egern' && // ua.match(/build\/(\d+)/i)?.[1] >= 718 // ) { // return true; // } // // if ( // // platform === 'Stash' && // // target === 'Stash' && // // gte(version, '3.1.0') // // ) { // // return true; // // } // // if ( // // platform === 'Loon' && // // target === 'Loon' && // // gte(version, '842.0.0') // // ) { // // return true; // // } } catch (e) { // $.error(`获取版本号失败: ${e}`); } return false; } ================================================ FILE: backend/src/utils/yaml.js ================================================ import YAML from 'static-js-yaml'; function retry(fn, content, ...args) { try { return fn(content, ...args); } catch (e) { return fn( dump( fn( content.replace(/!\s*/g, '__SubStoreJSYAMLString__'), ...args, ), ).replace(/__SubStoreJSYAMLString__/g, ''), ...args, ); } } export function safeLoad(content, ...args) { return retry(YAML.safeLoad, JSON.parse(JSON.stringify(content)), ...args); } export function load(content, ...args) { return retry(YAML.load, JSON.parse(JSON.stringify(content)), ...args); } export function safeDump(content, ...args) { return YAML.safeDump(JSON.parse(JSON.stringify(content)), ...args); } export function dump(content, ...args) { return YAML.dump(JSON.parse(JSON.stringify(content)), ...args); } export default { safeLoad, load, safeDump, dump, parse: safeLoad, stringify: safeDump, }; ================================================ FILE: backend/src/vendor/express.js ================================================ /* eslint-disable no-undef */ import { ENV } from './open-api'; export default function express({ substore: $, port, host }) { const { isNode } = ENV(); const DEFAULT_HEADERS = { 'Content-Type': 'text/plain;charset=UTF-8', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept', 'X-Powered-By': isNode ? eval('process.env.SUB_STORE_X_POWERED_BY') || 'Sub-Store' : 'Sub-Store', }; // node support if (isNode) { const express_ = eval(`require("express")`); const bodyParser = eval(`require("body-parser")`); const app = express_(); const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb'; $.info(`[BACKEND] body JSON limit: ${limit}`); app.use( bodyParser.json({ verify: rawBodySaver, limit, }), ); app.use( bodyParser.urlencoded({ verify: rawBodySaver, extended: true }), ); app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' })); app.use((req, res, next) => { const originalSetHeader = res.setHeader.bind(res); res.setHeader = function (name, value) { function normalize(v) { if (typeof v !== 'string') return v; if (['profile-web-page-url'].includes(name.toLowerCase())) { try { const url = new URL(v); return url.href; // 自动 punycode + 标准化 } catch { return v; } } return v; } try { if (Array.isArray(value)) { value = value.map(normalize); } else { value = normalize(value); } return originalSetHeader(name, value); } catch (err) { console.log(`Invalid header ignored\n${name}: ${value}`); return this; } }; next(); }); app.use((_, res, next) => { res.set(DEFAULT_HEADERS); next(); }); // adapter app.start = () => { app.get('*', function (req, res) { res.status(404).end(); }); const listener = app.listen(port, host, () => { const { address, port } = listener.address(); $.info(`[BACKEND] listening on ${address}:${port}`); }); }; return app; } // route handlers const handlers = []; // http methods const METHODS_NAMES = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', "HEAD'", 'ALL', ]; // dispatch url to route const dispatch = (request, start = 0) => { let { method, url, headers, body } = request; headers = formatHeaders(headers); if (/json/i.test(headers['content-type'])) { body = JSON.parse(body); } method = method.toUpperCase(); const { path, query } = extractURL(url); // pattern match let handler = null; let i; let longestMatchedPattern = 0; for (i = start; i < handlers.length; i++) { if (handlers[i].method === 'ALL' || method === handlers[i].method) { const { pattern } = handlers[i]; if (patternMatched(pattern, path)) { if (pattern.split('/').length > longestMatchedPattern) { handler = handlers[i]; longestMatchedPattern = pattern.split('/').length; } } } } if (handler) { // dispatch to next handler const next = () => { dispatch(method, url, i); }; const req = { method, url, path, query, params: extractPathParams(handler.pattern, path), headers, body, }; const res = Response(); const cb = handler.callback; const errFunc = (err) => { res.status(500).json({ status: 'failed', message: `Internal Server Error: ${err}`, }); }; if (cb.constructor.name === 'AsyncFunction') { cb(req, res, next).catch(errFunc); } else { try { cb(req, res, next); } catch (err) { errFunc(err); } } } else { // no route, return 404 const res = Response(); res.status(404).json({ status: 'failed', message: 'ERROR: 404 not found', }); } }; const app = {}; // attach http methods METHODS_NAMES.forEach((method) => { app[method.toLowerCase()] = (pattern, callback) => { // add handler handlers.push({ method, pattern, callback }); }; }); // chainable route app.route = (pattern) => { const chainApp = {}; METHODS_NAMES.forEach((method) => { chainApp[method.toLowerCase()] = (callback) => { // add handler handlers.push({ method, pattern, callback }); return chainApp; }; }); return chainApp; }; // start service app.start = () => { dispatch($request); }; return app; /************************************************ Utility Functions *************************************************/ function rawBodySaver(req, res, buf, encoding) { if (buf && buf.length) { req.rawBody = buf.toString(encoding || 'utf8'); } } function Response() { let statusCode = 200; const { isQX, isLoon, isSurge, isGUIforCores } = ENV(); const headers = DEFAULT_HEADERS; const STATUS_CODE_MAP = { 200: 'HTTP/1.1 200 OK', 201: 'HTTP/1.1 201 Created', 302: 'HTTP/1.1 302 Found', 307: 'HTTP/1.1 307 Temporary Redirect', 308: 'HTTP/1.1 308 Permanent Redirect', 404: 'HTTP/1.1 404 Not Found', 500: 'HTTP/1.1 500 Internal Server Error', }; return new (class { status(code) { statusCode = code; return this; } send(body = '') { const response = { status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode, body, headers, }; if (isQX || isGUIforCores) { $done(response); } else if (isLoon || isSurge) { $done({ response, }); } } end() { this.send(); } html(data) { this.set('Content-Type', 'text/html;charset=UTF-8'); this.send(data); } json(data) { this.set('Content-Type', 'application/json;charset=UTF-8'); this.send(JSON.stringify(data)); } set(key, val) { headers[key] = val; return this; } removeHeader(key) { delete headers[key]; return this; } })(); } } function formatHeaders(headers) { const result = {}; for (const k of Object.keys(headers)) { result[k.toLowerCase()] = headers[k]; } return result; } function patternMatched(pattern, path) { if (pattern instanceof RegExp && pattern.test(path)) { return true; } else { // root pattern, match all if (pattern === '/') return true; // normal string pattern if (pattern.indexOf(':') === -1) { const spath = path.split('/'); const spattern = pattern.split('/'); for (let i = 0; i < spattern.length; i++) { if (spath[i] !== spattern[i]) { return false; } } return true; } else if (extractPathParams(pattern, path)) { // string pattern with path parameters return true; } } return false; } function extractURL(url) { // extract path const match = url.match(/https?:\/\/[^/]+(\/[^?]*)/) || []; const path = match[1] || '/'; // extract query string const split = url.indexOf('?'); const query = {}; if (split !== -1) { let hashes = url.slice(url.indexOf('?') + 1).split('&'); for (let i = 0; i < hashes.length; i++) { const hash = hashes[i].split('='); query[hash[0]] = decodeURIComponent(hash[1]); } } return { path, query, }; } function extractPathParams(pattern, path) { if (pattern.indexOf(':') === -1) { return null; } else { const params = {}; for (let i = 0, j = 0; i < pattern.length; i++, j++) { if (pattern[i] === ':') { let key = []; let val = []; while (pattern[++i] !== '/' && i < pattern.length) { key.push(pattern[i]); } while (path[j] !== '/' && j < path.length) { val.push(path[j++]); } params[key.join('')] = decodeURIComponent(val.join('')); } else { if (pattern[i] !== path[j]) { return null; } } } return params; } } ================================================ FILE: backend/src/vendor/md5.js ================================================ /* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet * Distributed under the BSD License * See http://pajhome.org.uk/crypt/md5 for more info. */ /* * Configurable variables. You may need to tweak these to be compatible with * the server-side, but the defaults work in most cases. */ var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ var b64pad = ''; /* base-64 pad character. "=" for strict RFC compliance */ /* * These are the functions you'll usually want to call * They take string arguments and return either hex or base-64 encoded strings */ export function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); } export function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); } export function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); } export function hex_hmac_md5(k, d) { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } export function b64_hmac_md5(k, d) { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } export function any_hmac_md5(k, d, e) { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); } /* * Perform a simple self-test to see if the VM is working */ function md5_vm_test() { return hex_md5('abc').toLowerCase() == '900150983cd24fb0d6963f7d28e17f72'; } /* * Calculate the MD5 of a raw string */ function rstr_md5(s) { return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); } /* * Calculate the HMAC-MD5, of a key and some data (raw strings) */ function rstr_hmac_md5(key, data) { var bkey = rstr2binl(key); if (bkey.length > 16) bkey = binl_md5(bkey, key.length * 8); var ipad = Array(16), opad = Array(16); for (var i = 0; i < 16; i++) { ipad[i] = bkey[i] ^ 0x36363636; opad[i] = bkey[i] ^ 0x5c5c5c5c; } var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); } /* * Convert a raw string to a hex string */ function rstr2hex(input) { try { hexcase; } catch (e) { hexcase = 0; } var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef'; var output = ''; var x; for (var i = 0; i < input.length; i++) { x = input.charCodeAt(i); output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f); } return output; } /* * Convert a raw string to a base-64 string */ function rstr2b64(input) { try { b64pad; } catch (e) { b64pad = ''; } var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var output = ''; var len = input.length; for (var i = 0; i < len; i += 3) { var triplet = (input.charCodeAt(i) << 16) | (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) | (i + 2 < len ? input.charCodeAt(i + 2) : 0); for (var j = 0; j < 4; j++) { if (i * 8 + j * 6 > input.length * 8) output += b64pad; else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f); } } return output; } /* * Convert a raw string to an arbitrary string encoding */ function rstr2any(input, encoding) { var divisor = encoding.length; var i, j, q, x, quotient; /* Convert to an array of 16-bit big-endian values, forming the dividend */ var dividend = Array(Math.ceil(input.length / 2)); for (i = 0; i < dividend.length; i++) { dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); } /* * Repeatedly perform a long division. The binary array forms the dividend, * the length of the encoding is the divisor. Once computed, the quotient * forms the dividend for the next step. All remainders are stored for later * use. */ var full_length = Math.ceil( (input.length * 8) / (Math.log(encoding.length) / Math.log(2)), ); var remainders = Array(full_length); for (j = 0; j < full_length; j++) { quotient = Array(); x = 0; for (i = 0; i < dividend.length; i++) { x = (x << 16) + dividend[i]; q = Math.floor(x / divisor); x -= q * divisor; if (quotient.length > 0 || q > 0) quotient[quotient.length] = q; } remainders[j] = x; dividend = quotient; } /* Convert the remainders to the output string */ var output = ''; for (i = remainders.length - 1; i >= 0; i--) output += encoding.charAt(remainders[i]); return output; } /* * Encode a string as utf-8. * For efficiency, this assumes the input is valid utf-16. */ function str2rstr_utf8(input) { var output = ''; var i = -1; var x, y; while (++i < input.length) { /* Decode utf-16 surrogate pairs */ x = input.charCodeAt(i); y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) { x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff); i++; } /* Encode output as utf-8 */ if (x <= 0x7f) output += String.fromCharCode(x); else if (x <= 0x7ff) output += String.fromCharCode( 0xc0 | ((x >>> 6) & 0x1f), 0x80 | (x & 0x3f), ); else if (x <= 0xffff) output += String.fromCharCode( 0xe0 | ((x >>> 12) & 0x0f), 0x80 | ((x >>> 6) & 0x3f), 0x80 | (x & 0x3f), ); else if (x <= 0x1fffff) output += String.fromCharCode( 0xf0 | ((x >>> 18) & 0x07), 0x80 | ((x >>> 12) & 0x3f), 0x80 | ((x >>> 6) & 0x3f), 0x80 | (x & 0x3f), ); } return output; } /* * Encode a string as utf-16 */ function str2rstr_utf16le(input) { var output = ''; for (var i = 0; i < input.length; i++) output += String.fromCharCode( input.charCodeAt(i) & 0xff, (input.charCodeAt(i) >>> 8) & 0xff, ); return output; } function str2rstr_utf16be(input) { var output = ''; for (var i = 0; i < input.length; i++) output += String.fromCharCode( (input.charCodeAt(i) >>> 8) & 0xff, input.charCodeAt(i) & 0xff, ); return output; } /* * Convert a raw string to an array of little-endian words * Characters >255 have their high-byte silently ignored. */ function rstr2binl(input) { var output = Array(input.length >> 2); for (var i = 0; i < output.length; i++) output[i] = 0; for (var i = 0; i < input.length * 8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32; return output; } /* * Convert an array of little-endian words to a string */ function binl2rstr(input) { var output = ''; for (var i = 0; i < input.length * 32; i += 8) output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff); return output; } /* * Calculate the MD5 of an array of little-endian words, and a bit length. */ function binl_md5(x, len) { /* append padding */ x[len >> 5] |= 0x80 << len % 32; x[(((len + 64) >>> 9) << 4) + 14] = len; var a = 1732584193; var b = -271733879; var c = -1732584194; var d = 271733878; for (var i = 0; i < x.length; i += 16) { var olda = a; var oldb = b; var oldc = c; var oldd = d; a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936); d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302); a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222); c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844); d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); a = safe_add(a, olda); b = safe_add(b, oldb); c = safe_add(c, oldc); d = safe_add(d, oldd); } return Array(a, b, c, d); } /* * These functions implement the four basic operations the algorithm uses. */ function md5_cmn(q, a, b, x, s, t) { return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); } function md5_ff(a, b, c, d, x, s, t) { return md5_cmn((b & c) | (~b & d), a, b, x, s, t); } function md5_gg(a, b, c, d, x, s, t) { return md5_cmn((b & d) | (c & ~d), a, b, x, s, t); } function md5_hh(a, b, c, d, x, s, t) { return md5_cmn(b ^ c ^ d, a, b, x, s, t); } function md5_ii(a, b, c, d, x, s, t) { return md5_cmn(c ^ (b | ~d), a, b, x, s, t); } /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. */ function safe_add(x, y) { var lsw = (x & 0xffff) + (y & 0xffff); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); } /* * Bitwise rotate a 32-bit number to the left. */ function bit_rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } ================================================ FILE: backend/src/vendor/open-api.js ================================================ /* eslint-disable no-undef */ const isQX = typeof $task !== 'undefined'; const isLoon = typeof $loon !== 'undefined'; // 可能有一些兼容环境依赖于这个, 先不改成 $environment.surge-version const isSurge = typeof $httpClient !== 'undefined' && !isLoon; const isNode = eval(`typeof process !== "undefined"`); // eval is needed in order to avoid browserify processing const isStash = 'undefined' !== typeof $environment && $environment['stash-version']; const isShadowRocket = 'undefined' !== typeof $rocket; const isEgern = 'undefined' !== typeof Egern && Egern.version; const isLanceX = 'undefined' != typeof $native; const isGUIforCores = typeof $Plugins !== 'undefined'; import { Base64 } from 'js-base64'; function isPlainObject(obj) { return ( obj !== null && typeof obj === 'object' && [null, Object.prototype].includes(Object.getPrototypeOf(obj)) ); } function parseSocks5Uri(uri) { // eslint-disable-next-line no-unused-vars let [__, username, password, server, port, query, name] = uri.match( /^socks5:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/, ); if (port) { port = parseInt(port, 10); } else { $.error(`port is not present in line: ${uri}`); throw new Error(`port is not present in line: ${uri}`); } return { type: 5, host: server, port, userId: username != null ? decodeURIComponent(username) : undefined, password: password != null ? decodeURIComponent(password) : undefined, }; } export class OpenAPI { constructor(name = 'untitled', debug = false) { this.name = name; this.debug = debug; this.http = HTTP(); this.env = ENV(); if (isNode) { const dotenv = eval(`require("dotenv")`); dotenv.config(); } this.node = (() => { if (isNode) { const fs = eval("require('fs')"); return { fs, }; } else { return null; } })(); this.initCache(); const delay = (t, v) => new Promise(function (resolve) { setTimeout(resolve.bind(null, v), t); }); Promise.prototype.delay = async function (t) { const v = await this; return await delay(t, v); }; } // persistence // initialize cache initCache() { if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}'); if (isLoon || isSurge) this.cache = JSON.parse($persistentStore.read(this.name) || '{}'); if (isGUIforCores) this.cache = JSON.parse( $Plugins.SubStoreCache.get(this.name) || '{}', ); if (isNode) { // create a json for root cache const basePath = eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.'; let rootPath = `${basePath}/root.json`; const backupRootPath = `${basePath}/root_${Date.now()}.json`; this.log(`Root path: ${rootPath}`); if (this.node.fs.existsSync(rootPath)) { try { this.root = JSON.parse( this.node.fs.readFileSync(`${rootPath}`), ); } catch (e) { this.node.fs.copyFileSync(rootPath, backupRootPath); this.error( `Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`, ); } } if (!isPlainObject(this.root)) { this.node.fs.writeFileSync(rootPath, JSON.stringify({}), { flag: 'w', }); this.root = {}; } // create a json file with the given name if not exists let fpath = `${basePath}/${this.name}.json`; const backupPath = `${basePath}/${this.name}_${Date.now()}.json`; this.log(`Data path: ${fpath}`); if (this.node.fs.existsSync(fpath)) { try { this.cache = JSON.parse( this.node.fs.readFileSync(`${fpath}`, 'utf-8'), ); if (!isPlainObject(this.cache)) throw new Error('Invalid Data'); } catch (e) { try { const str = Base64.decode( this.node.fs.readFileSync(`${fpath}`, 'utf-8'), ); this.cache = JSON.parse(str); this.node.fs.writeFileSync(fpath, str, { flag: 'w', }); if (!isPlainObject(this.cache)) throw new Error('Invalid Data'); } catch (e) { this.node.fs.copyFileSync(fpath, backupPath); this.error( `Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`, ); } } } if (!isPlainObject(this.cache)) { this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'w', }); this.cache = {}; } } } // store cache persistCache() { const data = JSON.stringify(this.cache, null, 2); if (isQX) $prefs.setValueForKey(data, this.name); if (isLoon || isSurge) $persistentStore.write(data, this.name); if (isGUIforCores) $Plugins.SubStoreCache.set(this.name, data); if (isNode) { const basePath = eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.'; this.node.fs.writeFileSync( `${basePath}/${this.name}.json`, data, { flag: 'w' }, (err) => console.log(err), ); this.node.fs.writeFileSync( `${basePath}/root.json`, JSON.stringify(this.root, null, 2), { flag: 'w' }, (err) => console.log(err), ); } } write(data, key) { this.log(`SET ${key}`); if (key.indexOf('#') !== -1) { key = key.substr(1); if (isSurge || isLoon) { return $persistentStore.write(data, key); } if (isQX) { return $prefs.setValueForKey(data, key); } if (isNode) { this.root[key] = data; } if (isGUIforCores) { return $Plugins.SubStoreCache.set(key, data); } } else { this.cache[key] = data; } this.persistCache(); } read(key) { this.log(`READ ${key}`); if (key.indexOf('#') !== -1) { key = key.substr(1); if (isSurge || isLoon) { return $persistentStore.read(key); } if (isQX) { return $prefs.valueForKey(key); } if (isNode) { return this.root[key]; } if (isGUIforCores) { return $Plugins.SubStoreCache.get(key); } } else { return this.cache[key]; } } delete(key) { this.log(`DELETE ${key}`); if (key.indexOf('#') !== -1) { key = key.substr(1); if (isSurge || isLoon) { return $persistentStore.write(null, key); } if (isQX) { return $prefs.removeValueForKey(key); } if (isNode) { delete this.root[key]; } if (isGUIforCores) { return $Plugins.SubStoreCache.remove(key); } } else { delete this.cache[key]; } this.persistCache(); } // notification notify(title, subtitle = '', content = '', options = {}) { const openURL = options['open-url']; const mediaURL = options['media-url']; if (isQX) $notify(title, subtitle, content, options); if (isSurge) { $notification.post( title, subtitle, content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`, { url: openURL, }, ); } if (isLoon) { let opts = {}; if (openURL) opts['openUrl'] = openURL; if (mediaURL) opts['mediaUrl'] = mediaURL; if (JSON.stringify(opts) === '{}') { $notification.post(title, subtitle, content); } else { $notification.post(title, subtitle, content, opts); } } if (isNode) { const content_ = content + (openURL ? `\n点击跳转: ${openURL}` : '') + (mediaURL ? `\n多媒体: ${mediaURL}` : ''); console.log(`${title}\n${subtitle}\n${content_}\n\n`); let push = eval('process.env.SUB_STORE_PUSH_SERVICE'); if (push) { if (/^https?:\/\//.test(push)) { // 处理 HTTP/HTTPS URL const url = push .replace( '[推送标题]', encodeURIComponent(title || 'Sub-Store'), ) .replace( '[推送内容]', encodeURIComponent( [subtitle, content_].map((i) => i).join('\n'), ), ); const $http = HTTP(); $http .get({ url }) .then((resp) => { console.log( `[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`, ); }) .catch((e) => { console.log( `[Push Service] URL: ${url}\nERROR: ${e}`, ); }); } else { const { execFile } = eval(`require("child_process")`); execFile( 'shoutrrr', [ 'send', '--url', push, '--message', `${title}\n${subtitle}\n${content_}`, ], (error, stdout, stderr) => { if (error) { console.log( `[Push Service] URL: ${push}\nERROR: ${error}`, ); return; } if (stderr) { console.log( `[Push Service] URL: ${push}\nstderr: ${stderr}`, ); } console.log( `[Push Service] URL: ${push}\nstdout: ${stdout}`, ); }, ); } } } if (isGUIforCores) { $Plugins.Notify(title, subtitle + '\n' + content); } } // other helper functions log(msg) { if (this.debug) console.log(`[${this.name}] LOG: ${msg}`); } info(msg) { console.log(`[${this.name}] INFO: ${msg}`); } error(msg) { console.log(`[${this.name}] ERROR: ${msg}`); } wait(millisec) { return new Promise((resolve) => setTimeout(resolve, millisec)); } done(value = {}) { if (isQX || isLoon || isSurge || isGUIforCores) { $done(value); } else if (isNode) { if (typeof $context !== 'undefined') { $context.headers = value.headers; $context.statusCode = value.statusCode; $context.body = value.body; } } } } export function ENV() { return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket, isEgern, isLanceX, isGUIforCores, }; } export function HTTP(defaultOptions = { baseURL: '' }) { const { isQX, isLoon, isSurge, isNode, isGUIforCores } = ENV(); const methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH', ]; const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; function send(method, options) { options = typeof options === 'string' ? { url: options } : options; const baseURL = defaultOptions.baseURL; if (baseURL && !URL_REGEX.test(options.url || '')) { options.url = baseURL ? baseURL + options.url : options.url; } options = { ...defaultOptions, ...options }; const timeout = options.timeout; const events = { ...{ onRequest: () => {}, onResponse: (resp) => resp, onTimeout: () => {}, }, ...options.events, }; events.onRequest(method, options); if (options.node) { // Surge & Loon allow connecting to a server using a specified proxy node if (isSurge) { const build = $environment['surge-build']; if (build && parseInt(build) >= 2407) { options['policy-descriptor'] = options.node; delete options.node; } } } let worker; if (isQX) { worker = $task.fetch({ method, url: options.url, headers: options.headers, body: options.body, opts: options.opts, }); } else if (isLoon || isSurge || isNode) { worker = new Promise(async (resolve, reject) => { const body = options.body; const opts = JSON.parse(JSON.stringify(options)); opts.body = body; opts.timeout = opts.timeout || 8000; if (opts.timeout) { opts.timeout++; if (isNaN(opts.timeout)) { opts.timeout = 8000; } if (!isNode) { let unit = 'ms'; // 这些客户端单位为 s if (isSurge || isStash || isShadowRocket) { opts.timeout = Math.ceil(opts.timeout / 1000); unit = 's'; } // Loon 为 ms // console.log(`[httpClient timeout] ${opts.timeout}${unit}`); } } if (isNode) { const undici = eval("require('undici')"); const { socksDispatcher } = eval("require('fetch-socks')"); const { ProxyAgent, EnvHttpProxyAgent, request, interceptors, } = undici; const agentOpts = { connect: { rejectUnauthorized: opts.strictSSL === false || opts.insecure === true || opts.rejectUnauthorized === false ? false : true, }, bodyTimeout: opts.timeout, headersTimeout: opts.timeout, maxHeaderSize: eval('process.env.SUB_STORE_MAX_HEADER_SIZE') || 32 * 1024, }; const tlsOptions = { rejectUnauthorized: agentOpts.connect.rejectUnauthorized, }; opts.tls = { ...(opts.tls || {}), ...tlsOptions, }; try { const url = new URL(opts.url); if (url.username || url.password) { opts.headers = { ...(opts.headers || {}), Authorization: `Basic ${Buffer.from( `${url.username || ''}:${ url.password || '' }`, ).toString('base64')}`, }; } let dispatcher; if (!opts.proxy) { const allProxy = eval('process.env.all_proxy') || eval('process.env.ALL_PROXY'); if (allProxy && /^socks5:\/\//.test(allProxy)) { opts.proxy = allProxy; } } if (opts.proxy) { if (/^socks5:\/\//.test(opts.proxy)) { dispatcher = socksDispatcher( parseSocks5Uri(opts.proxy), { ...agentOpts, requestTls: tlsOptions, }, ); } else { dispatcher = new ProxyAgent({ ...agentOpts, uri: opts.proxy, requestTls: tlsOptions, }); } } else { dispatcher = new EnvHttpProxyAgent({ ...agentOpts, requestTls: tlsOptions, }); } const response = await request(opts.url, { ...opts, method: method.toUpperCase(), dispatcher: dispatcher.compose( interceptors.redirect({ maxRedirections: 3, throwOnMaxRedirects: true, }), ), }); resolve({ statusCode: response.statusCode, headers: response.headers, body: opts.encoding === null ? await response.body.arrayBuffer() : await response.body.text(), }); } catch (e) { reject(e); } } else { $httpClient[method.toLowerCase()]( opts, (err, response, body) => { // if (err) { // console.log(err); // } else { // console.log({ // statusCode: // response.status || response.statusCode, // headers: response.headers, // body, // }); // } if (err) reject(err); else resolve({ statusCode: response.status || response.statusCode, headers: response.headers, body, }); }, ); } }); } else if (isGUIforCores) { worker = new Promise(async (resolve, reject) => { try { const response = await $Plugins.Requests({ method, url: options.url, headers: options.headers, body: options.body, autoTransformBody: false, options: { Proxy: options.proxy, Timeout: options.timeout ? options.timeout / 1000 : 15, }, }); resolve({ statusCode: response.status, headers: response.headers, body: response.body, }); } catch (error) { reject(error); } }); } let timeoutid; const timer = timeout ? new Promise((_, reject) => { // console.log(`[request timeout] ${timeout}ms`); timeoutid = setTimeout(() => { events.onTimeout(); return reject( `${method} URL: ${options.url} exceeds the timeout ${timeout} ms`, ); }, timeout); }) : null; return ( timer ? Promise.race([timer, worker]).then((res) => { if (typeof clearTimeout !== 'undefined') { clearTimeout(timeoutid); } return res; }) : worker ).then((resp) => events.onResponse(resp)); } const http = {}; methods.forEach( (method) => (http[method.toLowerCase()] = (options) => send(method, options)), ); return http; } ================================================ FILE: config/Egern.yaml ================================================ name: Sub-Store description: "支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *" compat_arguments: ability: http-client-policy cronexp: 55 23 * * * sync: "Sub-Store Sync" timeout: 120 engine: auto produce: "# Sub-Store Produce" produce_cronexp: 50 */6 * * * produce_sub: "sub1,sub2" produce_col: "col1,col2" compat_arguments_desc: '\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅' scriptings: - http_request: name: Sub-Store Core match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js body_required: true - http_request: name: Sub-Store Simple match: ^https?:\/\/sub\.store script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js body_required: true - schedule: name: "{{{sync}}}" cron: "{{{cronexp}}}" script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js - schedule: name: "{{{produce}}}" cron: "{{{produce_cronexp}}}" script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js arguments: _compat.$argument: "sub={{{produce_sub}}}&col={{{produce_col}}}" mitm: hostnames: includes: - sub.store ================================================ FILE: config/Loon.plugin ================================================ #!name=Sub-Store #!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分, 可在插件设置中自定义. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置' #!openUrl=https://sub.store #!author=Peng-YM #!homepage=https://github.com/sub-store-org/Sub-Store #!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png [Argument] cron=input, "55 23 * * *", tag=定时参数, desc=这里需要输入符合CRON表达式的参数 [Rule] DOMAIN,sub-store.vercel.app,PROXY [MITM] hostname=sub.store [Script] http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple cron {cron} script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync ================================================ FILE: config/QX-Task.json ================================================ { "name": "Sub-Store", "description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'", "task": [ "55 23 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" ] } ================================================ FILE: config/QX.snippet ================================================ hostname=sub.store ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js ^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js ================================================ FILE: config/README.md ================================================ # Sub-Store 配置指南 ## 查看更新说明: Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases) Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts) ## 服务器/云平台/Docker/Android 版 https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87 ## App 版 ### 1. Loon 安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。 资源解析器中使用 [https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-parser.loon.min.js](https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-parser.loon.min.js) ### 2. Surge #### 关于 Surge 的格外说明 Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E) 定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449) 0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule) 1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) > 最新版 Surge 已删除 `ability: http-client-policy` 参数, 模块暂不做修改, 对测落地功能无影响 2. 经典版, 不支持编辑参数, 固定带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者 cname 脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule) 3. 经典版, 不支持编辑参数, 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) ### 3. QX 订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。 定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json) ### 4. Stash 安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。 ### 5. Shadowrocket 安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。 ### 6. Egern 安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。 ## 使用 Sub-Store 1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。 2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。 3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。 ## 链接参数说明 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E ## 脚本使用说明 https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E ================================================ FILE: config/Stash.stoverride ================================================ name: Sub-Store desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置' icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png http: mitm: - sub.store script: - match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) name: sub-store-1 type: request require-body: true timeout: 120 - match: ^https?:\/\/sub\.store name: sub-store-0 type: request require-body: true timeout: 120 cron: script: - name: cron-sync-artifacts cron: "55 23 * * *" timeout: 120 script-providers: sub-store-0: url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js interval: 86400 sub-store-1: url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js interval: 86400 cron-sync-artifacts: url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js interval: 86400 ================================================ FILE: config/Surge-Beta.sgmodule ================================================ #!name=Sub-Store(β) #!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * * #!category=订阅管理 #!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,produce_sub:"sub1,sub2",produce_col:"col1,col2",max_size:-1 #!arguments-desc=\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅 [MITM] hostname = %APPEND% sub.store [Script] Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}},max-size={{{max_size}}} Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}},max-size={{{max_size}}} {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}} {{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}" ================================================ FILE: config/Surge-Noability.sgmodule ================================================ #!name=Sub-Store #!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置' #!category=订阅管理 [MITM] hostname = %APPEND% sub.store [Script] # 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本 Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120 Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120 Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js ================================================ FILE: config/Surge-ability.sgmodule ================================================ #!name=Sub-Store #!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置' #!category=订阅管理 [MITM] hostname = %APPEND% sub.store [Script] Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120 Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js ================================================ FILE: config/Surge.sgmodule ================================================ #!name=Sub-Store #!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * * #!category=订阅管理 #!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,produce_sub:"sub1,sub2",produce_col:"col1,col2",max_size:-1 #!arguments-desc=\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅 [MITM] hostname = %APPEND% sub.store [Script] Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}},max-size={{{max_size}}} Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}},max-size={{{max_size}}} {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}} {{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}" ================================================ FILE: scripts/demo.js ================================================ function operator(proxies = [], targetPlatform, context) { // 支持快捷操作 不一定要写一个 function // 可参考 https://t.me/zhetengsha/970 // https://t.me/zhetengsha/1009 // proxies 为传入的内部节点数组 // 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅 // 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货. 顺便说一下, 关于 mihomo 不支持的协议, 其实也可以用 JSON/JSON5/YAML 格式来输入, 写法可参考使用 includeUnsupportedProxy 参数或开启 包含官方/商店版不支持的协议 开关时的 mihomo 输出内容, 例如 NaiveProxy 输入写法 (https://t.me/zhetengsha/4308) // 1. `_no-resolve` 为不解析域名 // 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功 // 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP // 4. 节点字段 _exec 为 mihomo 路径, 默认 /usr/local/bin/mihomo; 节点字段 _localPort 端口为初始端口号, 逐个递减, 默认为 65535. _defaultNameserver(默认为 [ '180.76.76.76', '52.80.52.52', '119.28.28.28', '223.6.6.6' ]) 和 _nameserver (默认为 [ 'https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query', 'https://doh-pure.onedns.net/dns-query' ]) 为数组 用于自定义mihomo 的 default-nameserver 和 nameserver, 这个是配置 Surge for macOS 必须手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议. 详见 https://t.me/zhetengsha/1735 // 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名 // 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名 // 7. `tls-fingerprint` 为 tls 指纹 // 8. `underlying-proxy` 为前置代理, 不同平台会自动转换 // 例如 $server['underlying-proxy'] = '名称' // 只给 mihomo 输出的话, `dialer-proxy` 也行 // 只给 sing-box 输出的话, `detour` 也行 // 只给 Egern 输出的话, `prev_hop` 也行 // 只给 Shadowrocket 输出的话, `chain` 也行 // 输出到 Clash/Stash 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能. // 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除 // 10. `sni` 在某些协议里会自动与 `servername` 转换 // 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512) // 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down` // 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时 // 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔 // 15. `ip-version` 设置节点使用 IP 版本,兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server` // 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp` // 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换 // 18. `sing-box` 支持 `_fragment`, `_fragment_fallback_delay`, `_record_fragment` 设置 `tls` 的 `fragment`, `fragment_fallback_delay`, `record_fragment` // 19. `sing-box` 支持 `_certificate`, `_certificate_path`, `_certificate_public_key_sha256`, `_client_certificate`, `_client_certificate_path`, `_client_key`, `_client_key_path` 设置 `tls` 的 `certificate`, `certificate_path`, `certificate_public_key_sha256`, `client_certificate`, `client_certificate_path`, `client_key`, `client_key_path` // 20. `sing-box` 支持使用完整的 `_ech` 结构设置 `tls` 的 `ech`. 避免冲突, URI 里的改为 _echConfigList // 21. `sing-box` 支持使用完整的 `_curve_preferences` 结构设置 `tls` 的 `curve_preferences` // 22. `interface-name` 指定流量出站接口 只给 Surge 用的话, `interface` 也可以 // require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块 // 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件 // const fs = eval(`require("fs")`) // // const path = eval(`require("path")`) // fs.writeFileSync('/tmp/1.txt', $content, "utf8"); // $arguments 为传入的脚本参数 // $options 为通过链接传入的参数 // 例如: { arg1: 'a', arg2: 'b' } // 可这样传: // 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' })) // /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D // 或这样传: // 先这样处理 encodeURIComponent('arg1=a&arg2=b') // /api/file/foo?$options=arg1%3Da%26arg2%3Db // 注意, 编辑页面左下角那个即可预览只是获取数据 并不是一个真实的请求, 故此时无法使用 $options // 默认会带上 _req 字段, 结构为 // { // method, // url, // path, // query, // params, // headers, // body, // } // console.log($options) // 若设置 $options._res.headers // 则会在输出时设置响应头, 例如: // if ($options) { // $options._res = { // headers: { // 'X-Custom': '1' // } // } // } // 若设置 $options._res.status // 则会在输出时设置响应状态码, 例如: // if ($options) { // $options._res = { // status: 404 // } // } // 一个示例: 请求来自分享且 ua 不符合时, 返回自定义状态码和响应内容 // const { headers, url, path } = $options?._req || {} // const ua = headers?.['user-agent'] || headers?.['User-Agent'] // if ($options && /^\/share\//.test(url) && !/surge/i.test(ua)) { // $options._res = { // status: 418 // } // $content = `I'm a teapot` // } // targetPlatform 为输出的目标平台 // lodash // $substore 为 OpenAPI // 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md // scriptResourceCache 缓存 // 可参考 https://t.me/zhetengsha/1003 // const cache = scriptResourceCache // 写入 // 第三个参数为自定义过期时间(单位: 毫秒) // cache.set('a:1', 1, 1000) // cache.set('a:2', 2) // 获取 // cache.get('a:1') // 获取到期时间 // cache.gettime('a:1') // 支持第二个参数: 自定义过期时间(单位: 毫秒) // 支持第三个参数: 是否删除过期项 // 下面的例子意思是原来是看 a:2 现在有没有到期的, 加了自定义过期时间后是看 +1000ms 会不会过期, 如果过期就删除 // cache.get('a:2', 1000, true) // 清理 // 本来是内部的 反正也能用...先这么用吧... // 清理所有过期的 // cache._cleanup() // 支持第一个参数: 匹配前缀的项 // 支持第二个参数: 自定义过期时间(单位: 毫秒) // 只清理 a: 开头的过期项 // cache._cleanup('a:') // 如果想删除所有的 a: 开头的过期项, 目前先传一个大的过期时间吧... // cache._cleanup(undefined, 48 * 3600 * 1000) // 下面的例子意思是原来是看现在有没有到期的, 加了自定义过期时间后是看 +1000ms 会不会过期, 如果过期就删除 // cache._cleanup(undefined, 1000) // 关于缓存时长 // 拉取 Sub-Store 订阅时, 会自动拉取远程订阅 // 通过链接下载资源时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true // 前端(>= 2.16.0) 后端(>= 2.21.0) 支持自定义各种缓存的 TTL 配置 // 持久化缓存数据在 JSON 里 // 当配合脚本使用时, 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活 // async function operator() { // scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000); // } // ProxyUtils 为节点处理工具 // 可参考 https://t.me/zhetengsha/1066 // const ProxyUtils = { // parse, // 订阅解析 // process, // 节点操作/文件操作 // produce, // 输出订阅 // getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000) // ipAddress, // https://github.com/beaugunderson/ip-address // isIPv4, // isIPv6, // isIP, // yaml, // yaml 解析和生成 // getFlag, // 获取 emoji 旗帜 // removeFlag, // 移除 emoji 旗帜 // getISO, // 获取 ISO 3166-1 alpha-2 代码 // Gist, // Gist 类 // download, // 内部的下载方法, 见 backend/src/utils/download.js // downloadFile, // 下载二进制文件, 见 backend/src/utils/download.js // MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269 // isValidUUID, // 辅助判断是否为有效的 UUID // Buffer, // https://github.com/feross/buffer // Base64, // https://github.com/dankogai/js-base64 // JSON5, // https://github.com/json5/json5 // } // 为兼容 https://github.com/xishang0128/sparkle 的 JavaScript 覆写, 也可以直接使用 `b64d`(Base64 解码), `b64e`(Base64 编码), `Buffer`, `yaml`(简单兼容了下 `yaml.parse` 和 `yaml.stringify`) // 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009 // ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一 // 示例: 给节点名添加前缀 // $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}` // 示例: 给节点名添加旗帜 // $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}` // 示例: 从 sni 文件中读取内容并进行节点操作 // const sni = await produceArtifact({ // type: 'file', // name: 'sni' // 文件名 // }); // $server.sni = sni // 示例: 从 config 文件中读取配置项并进行节点操作 // config 的本地内容为 // { // "reuse": false // } // 脚本操作为 // const config = (ProxyUtils.JSON5 || JSON).parse(await produceArtifact({ // type: 'file', // name: 'config' // 文件名 // })) // $server.reuse = config.reuse // 1. Surge 输出 WireGuard 完整配置 // let proxies = await produceArtifact({ // type: 'subscription', // name: 'sub', // platform: 'Surge', // produceOpts: { // 'include-unsupported-proxy': true, // } // }) // $content = proxies // 2. sing-box // 但是一般不需要这样用, 可参考 // 1. https://t.me/zhetengsha/1111 // 2. https://t.me/zhetengsha/1070 // 3. https://t.me/zhetengsha/1241 // let singboxProxies = await produceArtifact({ // type: 'subscription', // type: 'subscription' 或 'collection' // name: 'sub', // subscription name // platform: 'sing-box', // target platform // produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') ) // }) // // JSON // $content = JSON.stringify({}, null, 2) // 3. clash.meta // 但是一般不需要这样用, 可参考 // 1. https://t.me/zhetengsha/1111 // 2. https://t.me/zhetengsha/1070 // 3. https://t.me/zhetengsha/1234 // let clashMetaProxies = await produceArtifact({ // type: 'subscription', // name: 'sub', // platform: 'ClashMeta', // produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies ) // }) // 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist // 见 https://t.me/zhetengsha/1428 // // const content = ProxyUtils.produce([...proxies], platform) // // YAML // ProxyUtils.yaml.load('YAML String') // ProxyUtils.yaml.safeLoad('YAML String') // $content = ProxyUtils.yaml.safeDump({}) // $content = ProxyUtils.yaml.dump({}) // 一个往文件里插入本地节点的例子: // const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0]) // let clashMetaProxies = await produceArtifact({ // type: 'collection', // name: '机场', // platform: 'ClashMeta', // produceType: 'internal' // }) // yaml.proxies.unshift(...clashMetaProxies) // $content = ProxyUtils.yaml.dump(yaml) // { $content, $files, $options } will be passed to the next operator // $content is the final content of the file // flowUtils 为机场订阅流量信息处理工具 // 可参考: // 1. https://t.me/zhetengsha/948 // context 为传入的上下文, 可在多个脚本中共享使用 // 其中 env 为 环境信息, 包含运行版本和其他后端信息 // 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可) // 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上 // 若存在 `source._collection` 但 `source._collection.subscriptions` 中的 key 在 `source` 上不存在, 说明输出结果为组合订阅, 脚本设置在组合订阅上 // 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上 // 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称 // let name = '' // for (const [key, value] of Object.entries(context.source)) { // if (!key.startsWith('_')) { // name = value.displayName || value.name // break // } // } // if (!name) { // const collection = context.source._collection // name = collection.displayName || collection.name // } // 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为: // { // "source": { // "sub-1": { // "name": "sub-1", // "displayName": "", // "mergeSources": "", // "ignoreFailedRemoteSub": true, // "process": [], // "icon": "", // "source": "local", // "url": "", // "content": "", // "ua": "", // "display-name": "", // "useCacheForFailedRemoteSub": false // } // }, // "backend": "Node", // "version": "2.14.198" // } // 2. 输出组合订阅 collection-1 时, 该组合订阅中的脚本上下文为: // { // "source": { // "_collection": { // "name": "collection-1", // "displayName": "", // "mergeSources": "", // "ignoreFailedRemoteSub": false, // "icon": "", // "process": [], // "subscriptions": [ // "sub-1" // ], // "display-name": "" // } // }, // "backend": "Node", // "version": "2.14.198" // } // 3. 输出组合订阅 collection-1 时, 该组合订阅中的单条订阅 sub-1 中的某个脚本上下文为: // { // "source": { // "sub-1": { // "name": "sub-1", // "displayName": "", // "mergeSources": "", // "ignoreFailedRemoteSub": true, // "icon": "", // "process": [], // "source": "local", // "url": "", // "content": "", // "ua": "", // "display-name": "", // "useCacheForFailedRemoteSub": false // }, // "_collection": { // "name": "collection-1", // "displayName": "", // "mergeSources": "", // "ignoreFailedRemoteSub": false, // "icon": "", // "process": [], // "subscriptions": [ // "sub-1" // ], // "display-name": "" // } // }, // "backend": "Node", // "version": "2.14.198" // } // 参数说明 // 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E console.log(JSON.stringify(context, null, 2)); return proxies; } ================================================ FILE: scripts/fancy-characters.js ================================================ /** * 节点名改为花里胡哨字体,仅支持英文字符和数字 * * 【字体】 * 可参考:https://www.dute.org/weird-fonts * serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代) * * 【示例】 * 1️⃣ 设置所有格式为 "serif-bold" * #type=serif-bold * * 2️⃣ 设置字母格式为 "serif-bold",数字格式为 "circle-regular" * #type=serif-bold&num=circle-regular */ function operator(proxies) { const { type, num } = $arguments; const TABLE = { "serif-bold": ["𝟎","𝟏","𝟐","𝟑","𝟒","𝟓","𝟔","𝟕","𝟖","𝟗","𝐚","𝐛","𝐜","𝐝","𝐞","𝐟","𝐠","𝐡","𝐢","𝐣","𝐤","𝐥","𝐦","𝐧","𝐨","𝐩","𝐪","𝐫","𝐬","𝐭","𝐮","𝐯","𝐰","𝐱","𝐲","𝐳","𝐀","𝐁","𝐂","𝐃","𝐄","𝐅","𝐆","𝐇","𝐈","𝐉","𝐊","𝐋","𝐌","𝐍","𝐎","𝐏","𝐐","𝐑","𝐒","𝐓","𝐔","𝐕","𝐖","𝐗","𝐘","𝐙"] , "serif-italic": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "𝑎", "𝑏", "𝑐", "𝑑", "𝑒", "𝑓", "𝑔", "ℎ", "𝑖", "𝑗", "𝑘", "𝑙", "𝑚", "𝑛", "𝑜", "𝑝", "𝑞", "𝑟", "𝑠", "𝑡", "𝑢", "𝑣", "𝑤", "𝑥", "𝑦", "𝑧", "𝐴", "𝐵", "𝐶", "𝐷", "𝐸", "𝐹", "𝐺", "𝐻", "𝐼", "𝐽", "𝐾", "𝐿", "𝑀", "𝑁", "𝑂", "𝑃", "𝑄", "𝑅", "𝑆", "𝑇", "𝑈", "𝑉", "𝑊", "𝑋", "𝑌", "𝑍"], "serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝒂","𝒃","𝒄","𝒅","𝒆","𝒇","𝒈","𝒉","𝒊","𝒋","𝒌","𝒍","𝒎","𝒏","𝒐","𝒑","𝒒","𝒓","𝒔","𝒕","𝒖","𝒗","𝒘","𝒙","𝒚","𝒛","𝑨","𝑩","𝑪","𝑫","𝑬","𝑭","𝑮","𝑯","𝑰","𝑱","𝑲","𝑳","𝑴","𝑵","𝑶","𝑷","𝑸","𝑹","𝑺","𝑻","𝑼","𝑽","𝑾","𝑿","𝒀","𝒁"], "sans-serif-regular": ["𝟢", "𝟣", "𝟤", "𝟥", "𝟦", "𝟧", "𝟨", "𝟩", "𝟪", "𝟫", "𝖺", "𝖻", "𝖼", "𝖽", "𝖾", "𝖿", "𝗀", "𝗁", "𝗂", "𝗃", "𝗄", "𝗅", "𝗆", "𝗇", "𝗈", "𝗉", "𝗊", "𝗋", "𝗌", "𝗍", "𝗎", "𝗏", "𝗐", "𝗑", "𝗒", "𝗓", "𝖠", "𝖡", "𝖢", "𝖣", "𝖤", "𝖥", "𝖦", "𝖧", "𝖨", "𝖩", "𝖪", "𝖫", "𝖬", "𝖭", "𝖮", "𝖯", "𝖰", "𝖱", "𝖲", "𝖳", "𝖴", "𝖵", "𝖶", "𝖷", "𝖸", "𝖹"], "sans-serif-bold": ["𝟬","𝟭","𝟮","𝟯","𝟰","𝟱","𝟲","𝟳","𝟴","𝟵","𝗮","𝗯","𝗰","𝗱","𝗲","𝗳","𝗴","𝗵","𝗶","𝗷","𝗸","𝗹","𝗺","𝗻","𝗼","𝗽","𝗾","𝗿","𝘀","𝘁","𝘂","𝘃","𝘄","𝘅","𝘆","𝘇","𝗔","𝗕","𝗖","𝗗","𝗘","𝗙","𝗚","𝗛","𝗜","𝗝","𝗞","𝗟","𝗠","𝗡","𝗢","𝗣","𝗤","𝗥","𝗦","𝗧","𝗨","𝗩","𝗪","𝗫","𝗬","𝗭"], "sans-serif-italic": ["0","1","2","3","4","5","6","7","8","9","𝘢","𝘣","𝘤","𝘥","𝘦","𝘧","𝘨","𝘩","𝘪","𝘫","𝘬","𝘭","𝘮","𝘯","𝘰","𝘱","𝘲","𝘳","𝘴","𝘵","𝘶","𝘷","𝘸","𝘹","𝘺","𝘻","𝘈","𝘉","𝘊","𝘋","𝘌","𝘍","𝘎","𝘏","𝘐","𝘑","𝘒","𝘓","𝘔","𝘕","𝘖","𝘗","𝘘","𝘙","𝘚","𝘛","𝘜","𝘝","𝘞","𝘟","𝘠","𝘡"], "sans-serif-bold-italic": ["0","1","2","3","4","5","6","7","8","9","𝙖","𝙗","𝙘","𝙙","𝙚","𝙛","𝙜","𝙝","𝙞","𝙟","𝙠","𝙡","𝙢","𝙣","𝙤","𝙥","𝙦","𝙧","𝙨","𝙩","𝙪","𝙫","𝙬","𝙭","𝙮","𝙯","𝘼","𝘽","𝘾","𝘿","𝙀","𝙁","𝙂","𝙃","𝙄","𝙅","𝙆","𝙇","𝙈","𝙉","𝙊","𝙋","𝙌","𝙍","𝙎","𝙏","𝙐","𝙑","𝙒","𝙓","𝙔","𝙕"], "script-regular": ["0","1","2","3","4","5","6","7","8","9","𝒶","𝒷","𝒸","𝒹","ℯ","𝒻","ℊ","𝒽","𝒾","𝒿","𝓀","𝓁","𝓂","𝓃","ℴ","𝓅","𝓆","𝓇","𝓈","𝓉","𝓊","𝓋","𝓌","𝓍","𝓎","𝓏","𝒜","ℬ","𝒞","𝒟","ℰ","ℱ","𝒢","ℋ","ℐ","𝒥","𝒦","ℒ","ℳ","𝒩","𝒪","𝒫","𝒬","ℛ","𝒮","𝒯","𝒰","𝒱","𝒲","𝒳","𝒴","𝒵"], "script-bold": ["0","1","2","3","4","5","6","7","8","9","𝓪","𝓫","𝓬","𝓭","𝓮","𝓯","𝓰","𝓱","𝓲","𝓳","𝓴","𝓵","𝓶","𝓷","𝓸","𝓹","𝓺","𝓻","𝓼","𝓽","𝓾","𝓿","𝔀","𝔁","𝔂","𝔃","𝓐","𝓑","𝓒","𝓓","𝓔","𝓕","𝓖","𝓗","𝓘","𝓙","𝓚","𝓛","𝓜","𝓝","𝓞","𝓟","𝓠","𝓡","𝓢","𝓣","𝓤","𝓥","𝓦","𝓧","𝓨","𝓩"], "fraktur-regular": ["0","1","2","3","4","5","6","7","8","9","𝔞","𝔟","𝔠","𝔡","𝔢","𝔣","𝔤","𝔥","𝔦","𝔧","𝔨","𝔩","𝔪","𝔫","𝔬","𝔭","𝔮","𝔯","𝔰","𝔱","𝔲","𝔳","𝔴","𝔵","𝔶","𝔷","𝔄","𝔅","ℭ","𝔇","𝔈","𝔉","𝔊","ℌ","ℑ","𝔍","𝔎","𝔏","𝔐","𝔑","𝔒","𝔓","𝔔","ℜ","𝔖","𝔗","𝔘","𝔙","𝔚","𝔛","𝔜","ℨ"], "fraktur-bold": ["0","1","2","3","4","5","6","7","8","9","𝖆","𝖇","𝖈","𝖉","𝖊","𝖋","𝖌","𝖍","𝖎","𝖏","𝖐","𝖑","𝖒","𝖓","𝖔","𝖕","𝖖","𝖗","𝖘","𝖙","𝖚","𝖛","𝖜","𝖝","𝖞","𝖟","𝕬","𝕭","𝕮","𝕯","𝕰","𝕱","𝕲","𝕳","𝕴","𝕵","𝕶","𝕷","𝕸","𝕹","𝕺","𝕻","𝕼","𝕽","𝕾","𝕿","𝖀","𝖁","𝖂","𝖃","𝖄","𝖅"], "monospace-regular": ["𝟶","𝟷","𝟸","𝟹","𝟺","𝟻","𝟼","𝟽","𝟾","𝟿","𝚊","𝚋","𝚌","𝚍","𝚎","𝚏","𝚐","𝚑","𝚒","𝚓","𝚔","𝚕","𝚖","𝚗","𝚘","𝚙","𝚚","𝚛","𝚜","𝚝","𝚞","𝚟","𝚠","𝚡","𝚢","𝚣","𝙰","𝙱","𝙲","𝙳","𝙴","𝙵","𝙶","𝙷","𝙸","𝙹","𝙺","𝙻","𝙼","𝙽","𝙾","𝙿","𝚀","𝚁","𝚂","𝚃","𝚄","𝚅","𝚆","𝚇","𝚈","𝚉"], "double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","ℂ","𝔻","𝔼","𝔽","𝔾","ℍ","𝕀","𝕁","𝕂","𝕃","𝕄","ℕ","𝕆","ℙ","ℚ","ℝ","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐","ℤ"], "circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"], "square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"], "modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"], }; // charCode => index in `TABLE` const INDEX = { "48": 0, "49": 1, "50": 2, "51": 3, "52": 4, "53": 5, "54": 6, "55": 7, "56": 8, "57": 9, "65": 36, "66": 37, "67": 38, "68": 39, "69": 40, "70": 41, "71": 42, "72": 43, "73": 44, "74": 45, "75": 46, "76": 47, "77": 48, "78": 49, "79": 50, "80": 51, "81": 52, "82": 53, "83": 54, "84": 55, "85": 56, "86": 57, "87": 58, "88": 59, "89": 60, "90": 61, "97": 10, "98": 11, "99": 12, "100": 13, "101": 14, "102": 15, "103": 16, "104": 17, "105": 18, "106": 19, "107": 20, "108": 21, "109": 22, "110": 23, "111": 24, "112": 25, "113": 26, "114": 27, "115": 28, "116": 29, "117": 30, "118": 31, "119": 32, "120": 33, "121": 34, "122": 35 }; return proxies.map(p => { p.name = [...p.name].map(c => { if (/[a-zA-Z0-9]/.test(c)) { const code = c.charCodeAt(0); const index = INDEX[code]; if (isNumber(code) && num) { return TABLE[num][index]; } else { return TABLE[type][index]; } } return c; }).join(""); return p; }) } function isNumber(code) { return code >= 48 && code <= 57; } ================================================ FILE: scripts/ip-flag-node.js ================================================ const $ = $substore; const {onlyFlagIP = true} = $arguments async function operator(proxies) { const BATCH_SIZE = 10; let i = 0; while (i < proxies.length) { const batch = proxies.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(async proxy => { if (onlyFlagIP && !ProxyUtils.isIP(proxy.server)) return; try { // remove the original flag let proxyName = removeFlag(proxy.name); // query ip-api const countryCode = await queryIpApi(proxy); proxyName = getFlagEmoji(countryCode) + ' ' + proxyName; proxy.name = proxyName; } catch (err) { // TODO: } })); await sleep(1000); i += BATCH_SIZE; } return proxies; } async function queryIpApi(proxy) { const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0"; const headers = { "User-Agent": ua }; const result = new Promise((resolve, reject) => { const url = `http://ip-api.com/json/${encodeURIComponent(proxy.server)}?lang=zh-CN`; $.http.get({ url, headers, }).then(resp => { const data = JSON.parse(resp.body); if (data.status === "success") { resolve(data.countryCode); } else { reject(new Error(data.message)); } }).catch(err => { console.log(err); reject(err); }); }); return result; } function getFlagEmoji(countryCode) { const codePoints = countryCode .toUpperCase() .split('') .map(char => 127397 + char.charCodeAt()); return String .fromCodePoint(...codePoints) .replace(/🇹🇼/g, '🇨🇳'); } function removeFlag(str) { return str .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '') .trim(); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } ================================================ FILE: scripts/ip-flag.js ================================================ const RESOURCE_CACHE_KEY = '#sub-store-cached-resource'; const CACHE_EXPIRATION_TIME_MS = 10 * 60 * 1000; const $ = $substore; class ResourceCache { constructor(expires) { this.expires = expires; if (!$.read(RESOURCE_CACHE_KEY)) { $.write('{}', RESOURCE_CACHE_KEY); } this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY)); this._cleanup(); } _cleanup() { // clear obsolete cached resource let clear = false; Object.entries(this.resourceCache).forEach((entry) => { const [id, updated] = entry; if (!updated.time) { // clear old version cache delete this.resourceCache[id]; $.delete(`#${id}`); clear = true; } if (new Date().getTime() - updated.time > this.expires) { delete this.resourceCache[id]; clear = true; } }); if (clear) this._persist(); } revokeAll() { this.resourceCache = {}; this._persist(); } _persist() { $.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY); } get(id) { const updated = this.resourceCache[id] && this.resourceCache[id].time; if (updated && new Date().getTime() - updated <= this.expires) { return this.resourceCache[id].data; } return null; } set(id, value) { this.resourceCache[id] = { time: new Date().getTime(), data: value } this._persist(); } } const resourceCache = new ResourceCache(CACHE_EXPIRATION_TIME_MS); async function operator(proxies) { const { isLoon, isSurge } = $substore.env; let support = false; if (isLoon) { support = true; } else if (isSurge) { const build = $environment['surge-build']; if (build && parseInt(build) >= 2407) { support = true; } } if (support) { const batches = []; const BATCH_SIZE = 10; let i = 0; while (i < proxies.length) { const batch = proxies.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(async proxy => { try { // remove the original flag let proxyName = removeFlag(proxy.name); // query ip-api const countryCode = await queryIpApi(proxy); proxyName = getFlagEmoji(countryCode) + ' ' + proxyName; proxy.name = proxyName; } catch (err) { // TODO: } })); await sleep(1000); i += BATCH_SIZE; } } else { $.error(`IP Flag only supports Loon and Surge!`); } return proxies; } const tasks = new Map(); async function queryIpApi(proxy) { const id = getId(proxy); if (tasks.has(id)) { return tasks.get(id); } const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0"; const headers = { "User-Agent": ua }; const { isLoon } = $substore.env; const target = isLoon ? "Loon" : "Surge"; const result = new Promise((resolve, reject) => { const cached = resourceCache.get(id); if (cached) { resolve(cached); } const url = `http://ip-api.com/json`; let node = ProxyUtils.produce([proxy], target); // Loon 需要去掉节点名字 if (isLoon) { const s = node.indexOf("="); node = node.substring(s + 1); } $.http.get({ url, headers, node }).then(resp => { const body = resp.body; const data = JSON.parse(body); if (data.status === "success") { resourceCache.set(id, data.countryCode); resolve(data.countryCode); } else { reject(new Error(data.message)); } }).catch(err => { console.log(err); reject(err); }); }); tasks.set(id, result); return result; } function getId(proxy) { return MD5(`IP-FLAG-${proxy.server}-${proxy.port}`); } function getFlagEmoji(countryCode) { const codePoints = countryCode .toUpperCase() .split('') .map(char => 127397 + char.charCodeAt()); return String .fromCodePoint(...codePoints) .replace(/🇹🇼/g, '🇨🇳'); } function removeFlag(str) { return str .replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '') .trim(); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } var MD5 = function (d) { var r = M(V(Y(X(d), 8 * d.length))); return r.toLowerCase() }; function M(d) { for (var _, m = "0123456789ABCDEF", f = "", r = 0; r < d.length; r++)_ = d.charCodeAt(r), f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _); return f } function X(d) { for (var _ = Array(d.length >> 2), m = 0; m < _.length; m++)_[m] = 0; for (m = 0; m < 8 * d.length; m += 8)_[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; return _ } function V(d) { for (var _ = "", m = 0; m < 32 * d.length; m += 8)_ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); return _ } function Y(d, _) { d[_ >> 5] |= 128 << _ % 32, d[14 + (_ + 64 >>> 9 << 4)] = _; for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) { var h = m, t = f, g = r, e = i; f = md5_ii(f = md5_ii(f = md5_ii(f = md5_ii(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_ff(f = md5_ff(f = md5_ff(f = md5_ff(f, r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = safe_add(m, h), f = safe_add(f, t), r = safe_add(r, g), i = safe_add(i, e) } return Array(m, f, r, i) } function md5_cmn(d, _, m, f, r, i) { return safe_add(bit_rol(safe_add(safe_add(_, d), safe_add(f, i)), r), m) } function md5_ff(d, _, m, f, r, i, n) { return md5_cmn(_ & m | ~_ & f, d, _, r, i, n) } function md5_gg(d, _, m, f, r, i, n) { return md5_cmn(_ & f | m & ~f, d, _, r, i, n) } function md5_hh(d, _, m, f, r, i, n) { return md5_cmn(_ ^ m ^ f, d, _, r, i, n) } function md5_ii(d, _, m, f, r, i, n) { return md5_cmn(m ^ (_ | ~f), d, _, r, i, n) } function safe_add(d, _) { var m = (65535 & d) + (65535 & _); return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m } function bit_rol(d, _) { return d << _ | d >>> 32 - _ } ================================================ FILE: scripts/media-filter.js ================================================ ================================================ FILE: scripts/revert.js ================================================ const $ = API() $.write("{}", "#sub-store") $.done() function ENV(){const e="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:"undefined"!=typeof $task,isLoon:"undefined"!=typeof $loon,isSurge:"undefined"!=typeof $httpClient&&"undefined"!=typeof $utils,isBrowser:"undefined"!=typeof document,isNode:"function"==typeof require&&!e,isJSBox:e,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:o,isScriptable:n,isNode:i,isBrowser:r}=ENV(),u=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;const a={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(h=>a[h.toLowerCase()]=(a=>(function(a,h){h="string"==typeof h?{url:h}:h;const d=e.baseURL;d&&!u.test(h.url||"")&&(h.url=d?d+h.url:h.url),h.body&&h.headers&&!h.headers["Content-Type"]&&(h.headers["Content-Type"]="application/x-www-form-urlencoded");const l=(h={...e,...h}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...h.events};let f,p;if(c.onRequest(a,h),t)f=$task.fetch({method:a,...h});else if(s||o||i)f=new Promise((e,t)=>{(i?require("request"):$httpClient)[a.toLowerCase()](h,(s,o,n)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:n})})});else if(n){const e=new Request(h.url);e.method=a,e.headers=h.headers,e.body=h.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}else r&&(f=new Promise((e,t)=>{fetch(h.url,{method:a,headers:h.headers,body:h.body}).then(e=>e.json()).then(t=>e({statusCode:t.status,headers:t.headers,body:t.data})).catch(t)}));const y=l?new Promise((e,t)=>{p=setTimeout(()=>(c.onTimeout(),t(`${a} URL: ${h.url} exceeds the timeout ${l} ms`)),l)}):null;return(y?Promise.race([y,f]).then(e=>(clearTimeout(p),e)):f).then(e=>c.onResponse(e))})(h,a))),a}function API(e="untitled",t=!1){const{isQX:s,isLoon:o,isSurge:n,isNode:i,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(i){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(o||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),i){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(o||n)&&$persistentStore.write(e,this.name),i&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||o)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);i&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||o?$persistentStore.read(e):s?$prefs.valueForKey(e):i?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||o)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);i&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",a="",h={}){const d=h["open-url"],l=h["media-url"];if(s&&$notify(e,t,a,h),n&&$notification.post(e,t,a+`${l?"\n多媒体:"+l:""}`,{url:d}),o){let s={};d&&(s.openUrl=d),l&&(s.mediaUrl=l),"{}"===JSON.stringify(s)?$notification.post(e,t,a):$notification.post(e,t,a,s)}if(i||u){const s=a+(d?`\n点击跳转: ${d}`:"")+(l?`\n多媒体: ${l}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||n?$done(e):i&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)} ================================================ FILE: scripts/tls-fingerprint.js ================================================ /** * 为节点添加 tls 证书指纹 * 示例 * #fingerprint=... */ function operator(proxies) { const { fingerprint } = $arguments; proxies.forEach(proxy => { proxy['tls-fingerprint'] = fingerprint; }); return proxies; } ================================================ FILE: scripts/udp-filter.js ================================================ /** * 过滤 UDP 节点 */ function filter(proxies) { return proxies.map(p => p.udp); } ================================================ FILE: scripts/vmess-ws-obfs-host.js ================================================ /** * 为 VMess WebSocket 节点修改混淆 host * 示例 * #host=google.com */ function operator(proxies) { const { host } = $arguments; proxies.forEach(p => { if (p.type === 'vmess' && p.network === 'ws') { p["ws-opts"] = p["ws-opts"] || {}; p["ws-opts"]["headers"] = p["ws-opts"]["headers"] || {}; p["ws-opts"]["headers"]["Host"] = host; } }); return proxies; } ================================================ FILE: vs.code-workspace ================================================ { "folders": [ { "path": "." } ], "settings": {} }