mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fe9da6e02 |
@@ -1,54 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
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: "16"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
cd backend && pnpm i
|
||||
- name: Test
|
||||
run: |
|
||||
cd backend
|
||||
pnpm test
|
||||
- name: Build
|
||||
run: |
|
||||
cd backend
|
||||
pnpm run build
|
||||
- id: tag
|
||||
name: Generate release tag
|
||||
run: |
|
||||
cd backend
|
||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||
echo "::set-output name=release_tag::$SUBSTORE_RELEASE"
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||
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
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
.DS_Store
|
||||
# json config
|
||||
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?
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "web"]
|
||||
path = web
|
||||
url = https://github.com/sub-store-org/Sub-Store-Front-End.git
|
||||
@@ -1,663 +0,0 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2015 Ayuntamiento de Madrid
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
@@ -1,94 +0,0 @@
|
||||
<div align="center">
|
||||
<br>
|
||||
<img width="200" src="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" alt="Sub-Store">
|
||||
<br>
|
||||
<br>
|
||||
<h2 align="center">Sub-Store<h2>
|
||||
</div>
|
||||
|
||||
<p align="center" color="#6a737d">
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
||||
</p>
|
||||
|
||||
[](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml)     
|
||||
|
||||
[](https://www.buymeacoffee.com/PengYM)
|
||||
|
||||
Core functionalities:
|
||||
|
||||
1. Conversion among various formats.
|
||||
2. Subscription formatting.
|
||||
3. Collect multiple subscriptions in one URL.
|
||||
|
||||
## 1. Subscription Conversion
|
||||
|
||||
### Supported Input Formats
|
||||
|
||||
- [x] SS URI
|
||||
- [x] SSR URI
|
||||
- [x] SSD URI
|
||||
- [x] V2RayN URI
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP)
|
||||
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
|
||||
|
||||
### Supported Target Platforms
|
||||
|
||||
- [x] QX
|
||||
- [x] Loon
|
||||
- [x] Surge
|
||||
- [x] Stash & Clash
|
||||
- [x] ShadowRocket
|
||||
|
||||
## 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.
|
||||
|
||||
### Development
|
||||
|
||||
Go to `backend` and `web` directories, install node dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
1. In `backend`, run the backend server on http://localhost:3000
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
2. In`web`, start the vue-cli server
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
This project is under the GPL V3 LICENSE.
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
|
||||
|
||||
## 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!
|
||||
- Speicial thanks to @Orz-3 and @58xinian for their awesome icons.
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"babel-plugin-relative-path-import",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"rootPathPrefix": "@",
|
||||
"rootPathSuffix": "src"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"ignorePatterns": ["*.min.js", "src/vendor/*.js"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
|
||||
* ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
|
||||
* ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
|
||||
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
||||
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
|
||||
* @updated: <%= updated %>
|
||||
* @version: <%= pkg.version %>
|
||||
* @author: Peng-YM
|
||||
* @github: https://github.com/Peng-YM/Sub-Store
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
|
||||
-16
File diff suppressed because one or more lines are too long
Vendored
-16
File diff suppressed because one or more lines are too long
Vendored
-16
File diff suppressed because one or more lines are too long
-16
File diff suppressed because one or more lines are too long
@@ -1,118 +0,0 @@
|
||||
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;
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.14.23",
|
||||
"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",
|
||||
"build": "gulp"
|
||||
},
|
||||
"author": "Peng-YM",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"automerge": "1.0.1-preview.7",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"js-base64": "^3.7.2",
|
||||
"lodash": "^4.17.21",
|
||||
"request": "^2.88.2",
|
||||
"requests": "^0.3.0",
|
||||
"semver": "^7.3.7",
|
||||
"static-js-yaml": "^1.0.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"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",
|
||||
"axios": "^0.21.2",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Generated
-9941
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
export const SCHEMA_VERSION_KEY = 'schemaVersion';
|
||||
export const SETTINGS_KEY = 'settings';
|
||||
export const SUBS_KEY = 'subs';
|
||||
export const COLLECTIONS_KEY = 'collections';
|
||||
export const ARTIFACTS_KEY = 'artifacts';
|
||||
export const RULES_KEY = 'rules';
|
||||
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 CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
|
||||
export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; // cached-script-resource CSR
|
||||
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour
|
||||
@@ -1,4 +0,0 @@
|
||||
import { OpenAPI } from '@/vendor/open-api';
|
||||
|
||||
const $ = new OpenAPI('sub-store');
|
||||
export default $;
|
||||
@@ -1,212 +0,0 @@
|
||||
import download from '@/utils/download';
|
||||
import { isIPv4, isIPv6 } 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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function process(proxies, operators = [], targetPlatform) {
|
||||
for (const item of operators) {
|
||||
// process script
|
||||
let script;
|
||||
const $arguments = {};
|
||||
if (item.type.indexOf('Script') !== -1) {
|
||||
const { mode, content } = item.args;
|
||||
if (mode === 'link') {
|
||||
const url = content;
|
||||
// extract link arguments
|
||||
const rawArgs = url.split('#');
|
||||
if (rawArgs.length > 1) {
|
||||
for (const pair of rawArgs[1].split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1] || true;
|
||||
$arguments[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// if this is a remote script, download it
|
||||
try {
|
||||
script = await download(url.split('#')[0]);
|
||||
// $.info(`Script loaded: >>>\n ${script}`);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
// skip the script if download failed.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
script = content;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PROXY_PROCESSORS[item.type]) {
|
||||
$.error(`Unknown operator: "${item.type}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
$.info(
|
||||
`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,
|
||||
);
|
||||
} else {
|
||||
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
||||
}
|
||||
proxies = await ApplyProcessor(processor, proxies);
|
||||
}
|
||||
return proxies;
|
||||
}
|
||||
|
||||
function produce(proxies, targetPlatform) {
|
||||
const producer = PROXY_PRODUCERS[targetPlatform];
|
||||
if (!producer) {
|
||||
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
||||
}
|
||||
|
||||
// filter unsupported proxies
|
||||
proxies = proxies.filter(
|
||||
(proxy) =>
|
||||
!(proxy.supported && proxy.supported[targetPlatform] === false),
|
||||
);
|
||||
|
||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||
return proxies
|
||||
.map((proxy) => {
|
||||
try {
|
||||
return producer.produce(proxy);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Cannot produce proxy: ${JSON.stringify(
|
||||
proxy,
|
||||
null,
|
||||
2,
|
||||
)}\nReason: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.filter((line) => line.length > 0)
|
||||
.join('\n');
|
||||
} else if (producer.type === 'ALL') {
|
||||
return producer.produce(proxies);
|
||||
}
|
||||
}
|
||||
|
||||
export const ProxyUtils = {
|
||||
parse,
|
||||
process,
|
||||
produce,
|
||||
};
|
||||
|
||||
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 lastParse(proxy) {
|
||||
if (proxy.type === 'trojan') {
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
transportHost = Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost;
|
||||
if (transportHost) {
|
||||
proxy.sni = transportHost;
|
||||
}
|
||||
}
|
||||
if (!proxy.sni && !isIP(proxy.server)) {
|
||||
proxy.sni = proxy.server;
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
import { getIfNotBlank, isPresent, isNotBlank, getIfPresent } from '@/utils';
|
||||
import getSurgeParser from './peggy/surge';
|
||||
import getLoonParser from './peggy/loon';
|
||||
import getQXParser from './peggy/qx';
|
||||
import getTrojanURIParser from './peggy/trojan-uri';
|
||||
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
// 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];
|
||||
|
||||
const proxy = {
|
||||
name: decodeURIComponent(line.split('#')[1]),
|
||||
type: 'ss',
|
||||
};
|
||||
content = content.split('#')[0]; // strip proxy name
|
||||
// handle IPV4 and IPV6
|
||||
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
let userInfoStr = Base64.decode(content.split('@')[0]);
|
||||
if (!serverAndPortArray) {
|
||||
content = Base64.decode(content);
|
||||
userInfoStr = content.split('@')[0];
|
||||
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
}
|
||||
const serverAndPort = serverAndPortArray[1];
|
||||
const portIdx = serverAndPort.lastIndexOf(':');
|
||||
proxy.server = serverAndPort.substring(0, portIdx);
|
||||
proxy.port = serverAndPort.substring(portIdx + 1);
|
||||
|
||||
const userInfo = userInfoStr.split(':');
|
||||
proxy.cipher = userInfo[0];
|
||||
proxy.password = userInfo[1];
|
||||
|
||||
// handle obfs
|
||||
const idx = content.indexOf('?plugin=');
|
||||
if (idx !== -1) {
|
||||
const pluginInfo = (
|
||||
'plugin=' +
|
||||
decodeURIComponent(content.split('?plugin=')[1].split('&')[0])
|
||||
).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.obfs = 'v2ray-plugin';
|
||||
proxy['plugin-opts'] = {
|
||||
mode: 'websocket',
|
||||
host: getIfNotBlank(params['obfs-host']),
|
||||
path: getIfNotBlank(params.path),
|
||||
tls: getIfPresent(params.tls),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported plugin option: ${params.plugin}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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];
|
||||
const content = Base64.decode(line);
|
||||
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 {
|
||||
// V2rayN URI format
|
||||
const params = JSON.parse(content);
|
||||
const proxy = {
|
||||
name: params.ps,
|
||||
type: 'vmess',
|
||||
server: params.add,
|
||||
port: params.port,
|
||||
cipher: getIfPresent(params.scy, 'auto'),
|
||||
uuid: params.id,
|
||||
alterId: parseInt(getIfPresent(params.aid, 0)),
|
||||
tls: params.tls === 'tls' || params.tls === true,
|
||||
'skip-cert-verify': isPresent(params.verify_cert)
|
||||
? !params.verify_cert
|
||||
: undefined,
|
||||
};
|
||||
// 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 && proxy.sni) {
|
||||
proxy.sni = params.sni;
|
||||
}
|
||||
// handle obfs
|
||||
if (params.net === 'ws') {
|
||||
proxy.network = 'ws';
|
||||
proxy['ws-opts'] = {
|
||||
path: getIfNotBlank(params.path),
|
||||
headers: { Host: getIfNotBlank(params.host) },
|
||||
};
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
|
||||
// sni 优先级应高于 host
|
||||
if (proxy.tls && !proxy.sni && params.host) {
|
||||
proxy.sni = params.host;
|
||||
}
|
||||
}
|
||||
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 parser = getTrojanURIParser();
|
||||
const proxy = parser.parse(line);
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Clash_All() {
|
||||
const name = 'Clash Parser';
|
||||
const test = (line) => {
|
||||
try {
|
||||
JSON.parse(line);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const parse = (line) => {
|
||||
const proxy = JSON.parse(line);
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
throw new Error(
|
||||
`Clash does not support proxy with type: ${proxy.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
// handle vmess sni
|
||||
if (proxy.type === 'vmess') {
|
||||
proxy.sni = proxy.servername;
|
||||
delete proxy.servername;
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network === 'ws') {
|
||||
proxy.sni = proxy['ws-opts']?.headers?.Host;
|
||||
} else if (proxy.network === 'http') {
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
proxy.sni = Array.isArray(httpHost)
|
||||
? httpHost[0]
|
||||
: httpHost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_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_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 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_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 = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [
|
||||
URI_SS(),
|
||||
URI_SSR(),
|
||||
URI_VMess(),
|
||||
URI_Trojan(),
|
||||
Clash_All(),
|
||||
Surge_SS(),
|
||||
Surge_VMess(),
|
||||
Surge_Trojan(),
|
||||
Surge_Http(),
|
||||
Surge_Snell(),
|
||||
Surge_Tuic(),
|
||||
Surge_Socks5(),
|
||||
Loon_SS(),
|
||||
Loon_SSR(),
|
||||
Loon_VMess(),
|
||||
Loon_Vless(),
|
||||
Loon_Trojan(),
|
||||
Loon_Http(),
|
||||
QX_SS(),
|
||||
QX_SSR(),
|
||||
QX_VMess(),
|
||||
QX_Trojan(),
|
||||
QX_Http(),
|
||||
QX_Socks5(),
|
||||
];
|
||||
@@ -1,183 +0,0 @@
|
||||
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) {
|
||||
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/others)*{
|
||||
proxy.type = "ssr";
|
||||
// handle ssr obfs
|
||||
proxy.obfs = obfs.type;
|
||||
}
|
||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/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_host/tls_verification/vmess_alterId/fast_open/udp_relay/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_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vless";
|
||||
handleTransport();
|
||||
}
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
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-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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
||||
|
||||
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_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 host:domain { obfs.host = host; }
|
||||
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 host:domain { transport.host = host; }
|
||||
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); }
|
||||
|
||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; }
|
||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
// 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) {
|
||||
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/others)*{
|
||||
proxy.type = "ssr";
|
||||
// handle ssr obfs
|
||||
proxy.obfs = obfs.type;
|
||||
}
|
||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/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_host/tls_verification/vmess_alterId/fast_open/udp_relay/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_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vless";
|
||||
handleTransport();
|
||||
}
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
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-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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
||||
|
||||
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_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 host:domain { obfs.host = host; }
|
||||
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 host:domain { transport.host = host; }
|
||||
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); }
|
||||
|
||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; }
|
||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
@@ -1,184 +0,0 @@
|
||||
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/http/socks5) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan" equals address
|
||||
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/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_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* {
|
||||
if (proxy.protocol) {
|
||||
proxy.type = "ssr";
|
||||
// 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_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
http = "http" equals address
|
||||
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
socks5 = "socks5" equals address
|
||||
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/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-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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
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"); }
|
||||
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 "tls-host" equals sni:domain { proxy.sni = sni; }
|
||||
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(); }
|
||||
|
||||
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") { 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 host:domain { obfs.host = host; }
|
||||
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; }
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// 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/http/socks5) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan" equals address
|
||||
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/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_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* {
|
||||
if (proxy.protocol) {
|
||||
proxy.type = "ssr";
|
||||
// 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_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
http = "http" equals address
|
||||
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
socks5 = "socks5" equals address
|
||||
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/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-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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
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"); }
|
||||
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 "tls-host" equals sni:domain { proxy.sni = sni; }
|
||||
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(); }
|
||||
|
||||
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") { 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 host:domain { obfs.host = host; }
|
||||
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; }
|
||||
|
||||
uri = $[^,]+
|
||||
|
||||
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
@@ -1,207 +0,0 @@
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/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);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleWebsocket();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http" address (username password)? (fast_open/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/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);
|
||||
}
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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(""); }
|
||||
|
||||
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
||||
sni = comma "sni" equals sni:domain { 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()); }
|
||||
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
||||
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-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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
|
||||
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();
|
||||
})
|
||||
obfs["ws-headers"] = result;
|
||||
}
|
||||
ws_path = comma "ws-path" equals path:uri { obfs.path = path; }
|
||||
|
||||
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
|
||||
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
||||
uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||
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(""); }
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// 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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/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);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
}
|
||||
handleWebsocket();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http" address (username password)? (fast_open/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/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);
|
||||
}
|
||||
}
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_verification/sni/fast_open/tfo/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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(""); }
|
||||
|
||||
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
||||
sni = comma "sni" equals sni:domain { 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()); }
|
||||
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
||||
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-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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
|
||||
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();
|
||||
})
|
||||
obfs["ws-headers"] = result;
|
||||
}
|
||||
ws_path = comma "ws-path" equals path:uri { obfs.path = path; }
|
||||
|
||||
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
|
||||
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
||||
uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||
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(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
@@ -1,124 +0,0 @@
|
||||
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)* {
|
||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||
proxy.sni = params["sni"] || params["peer"];
|
||||
|
||||
if (toBool(params["ws"])) {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", params["wspath"]);
|
||||
}
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
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"]));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// 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)* {
|
||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||
proxy.sni = params["sni"] || params["peer"];
|
||||
|
||||
if (toBool(params["ws"])) {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", params["wspath"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
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"]));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { safeLoad } from 'static-js-yaml';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||
// simply discard HTML
|
||||
const parse = () => '';
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Base64Encoded() {
|
||||
const name = 'Base64 Pre-processor';
|
||||
|
||||
const keys = [
|
||||
'dm1lc3M',
|
||||
'c3NyOi8v',
|
||||
'dHJvamFu',
|
||||
'c3M6Ly',
|
||||
'c3NkOi8v',
|
||||
'c2hhZG93',
|
||||
'aHR0c',
|
||||
];
|
||||
|
||||
const test = function (raw) {
|
||||
return keys.some((k) => raw.indexOf(k) !== -1);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
raw = Base64.decode(raw);
|
||||
return raw;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Clash() {
|
||||
const name = 'Clash Pre-processor';
|
||||
const test = function (raw) {
|
||||
return /proxies/.test(raw);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
// Clash YAML format
|
||||
const proxies = safeLoad(raw).proxies;
|
||||
return proxies.map((p) => JSON.stringify(p)).join('\n');
|
||||
};
|
||||
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(), Base64Encoded(), Clash(), SSD(), FullConfig()];
|
||||
@@ -1,662 +0,0 @@
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import { FULL } from '@/utils/logical';
|
||||
import { getFlag } from '@/utils/geo';
|
||||
import lodash from 'lodash';
|
||||
import $ from '@/core/app';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
|
||||
/**
|
||||
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.filter((_, i) => selected[i]);
|
||||
}
|
||||
|
||||
return proxies.map((proxy) => {
|
||||
proxy.udp = get(args.udp, proxy.udp);
|
||||
proxy.tfo = get(args.tfo, proxy.tfo);
|
||||
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 }) {
|
||||
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;
|
||||
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
|
||||
}
|
||||
return proxy;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// duplicate handler
|
||||
function HandleDuplicateOperator(arg) {
|
||||
const { action, template, link, position } = {
|
||||
...{
|
||||
action: 'rename',
|
||||
template: '0 1 2 3 4 5 6 7 8 9',
|
||||
link: '-',
|
||||
position: 'back',
|
||||
},
|
||||
...arg,
|
||||
};
|
||||
return {
|
||||
name: 'Handle Duplicate Operator',
|
||||
func: (proxies) => {
|
||||
if (action === 'delete') {
|
||||
const chosen = {};
|
||||
return proxies.filter((p) => {
|
||||
if (chosen[p.name]) {
|
||||
return false;
|
||||
}
|
||||
chosen[p.name] = true;
|
||||
return true;
|
||||
});
|
||||
} else if (action === 'rename') {
|
||||
const numbers = template.split(' ');
|
||||
// count occurrences of each name
|
||||
const counter = {};
|
||||
let maxLen = 0;
|
||||
proxies.forEach((p) => {
|
||||
if (typeof counter[p.name] === 'undefined')
|
||||
counter[p.name] = 1;
|
||||
else counter[p.name]++;
|
||||
maxLen = Math.max(
|
||||
counter[p.name].toString().length,
|
||||
maxLen,
|
||||
);
|
||||
});
|
||||
const increment = {};
|
||||
return proxies.map((p) => {
|
||||
if (counter[p.name] > 1) {
|
||||
if (typeof increment[p.name] == 'undefined')
|
||||
increment[p.name] = 1;
|
||||
let num = '';
|
||||
let cnt = increment[p.name]++;
|
||||
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(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 ((!oA && !oB) || (oA && oB && oA === oB))
|
||||
return a.name < b.name ? -1 : 1; // fallback to normal sort
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
name: 'Script Operator',
|
||||
func: async (proxies) => {
|
||||
let output = proxies;
|
||||
await (async function () {
|
||||
const operator = createDynamicFunction(
|
||||
'operator',
|
||||
script,
|
||||
$arguments,
|
||||
);
|
||||
output = operator(proxies, targetPlatform);
|
||||
})();
|
||||
return output;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const DOMAIN_RESOLVERS = {
|
||||
Google: async function (domain) {
|
||||
const id = hex_md5(`GOOGLE:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
|
||||
domain,
|
||||
)}&type=A`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
});
|
||||
const body = JSON.parse(resp.body);
|
||||
if (body['Status'] !== 0) {
|
||||
throw new Error(`Status is ${body['Status']}`);
|
||||
}
|
||||
const answers = body['Answer'];
|
||||
if (answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1].data;
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
'IP-API': async function (domain) {
|
||||
const id = hex_md5(`IP-API:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||
domain,
|
||||
)}?lang=zh-CN`,
|
||||
});
|
||||
const body = JSON.parse(resp.body);
|
||||
if (body['status'] !== 'success') {
|
||||
throw new Error(`Status is ${body['status']}`);
|
||||
}
|
||||
const result = body.query;
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Cloudflare: async function (domain) {
|
||||
const id = hex_md5(`CLOUDFLARE:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
|
||||
domain,
|
||||
)}&type=A`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
});
|
||||
const body = JSON.parse(resp.body);
|
||||
if (body['Status'] !== 0) {
|
||||
throw new Error(`Status is ${body['Status']}`);
|
||||
}
|
||||
const answers = body['Answer'];
|
||||
if (answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1].data;
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
function ResolveDomainOperator({ provider }) {
|
||||
const resolver = DOMAIN_RESOLVERS[provider];
|
||||
if (!resolver) {
|
||||
throw new Error(`Cannot find resolver: ${provider}`);
|
||||
}
|
||||
return {
|
||||
name: 'Resolve Domain Operator',
|
||||
func: async (proxies) => {
|
||||
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)).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)
|
||||
.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((proxy) => {
|
||||
proxy.server = results[proxy.server] || proxy.server;
|
||||
});
|
||||
|
||||
return proxies;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
|
||||
ResolveDomainOperator.resolver = DOMAIN_RESOLVERS;
|
||||
|
||||
/**************************** Filters ***************************************/
|
||||
// filter useless proxies
|
||||
function UselessFilter() {
|
||||
const KEYWORDS = [
|
||||
'网址',
|
||||
'流量',
|
||||
'时间',
|
||||
'应急',
|
||||
'过期',
|
||||
'Bandwidth',
|
||||
'expire',
|
||||
];
|
||||
return {
|
||||
name: 'Useless Filter',
|
||||
func: RegexFilter({
|
||||
regex: KEYWORDS,
|
||||
keep: false,
|
||||
}).func,
|
||||
};
|
||||
}
|
||||
|
||||
// filter by regions
|
||||
function RegionFilter(regions) {
|
||||
const REGION_MAP = {
|
||||
HK: '🇭🇰',
|
||||
TW: '🇹🇼',
|
||||
US: '🇺🇸',
|
||||
SG: '🇸🇬',
|
||||
JP: '🇯🇵',
|
||||
UK: '🇬🇧',
|
||||
};
|
||||
return {
|
||||
name: 'Region Filter',
|
||||
func: (proxies) => {
|
||||
// this would be high memory usage
|
||||
return proxies.map((proxy) => {
|
||||
const flag = getFlag(proxy.name);
|
||||
return regions.some((r) => REGION_MAP[r] === flag);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 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(types) {
|
||||
return {
|
||||
name: 'Type Filter',
|
||||
func: (proxies) => {
|
||||
return proxies.map((proxy) => types.some((t) => proxy.type === t));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
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) {
|
||||
return {
|
||||
name: 'Script Filter',
|
||||
func: async (proxies) => {
|
||||
let output = FULL(proxies.length, true);
|
||||
await (async function () {
|
||||
const filter = createDynamicFunction(
|
||||
'filter',
|
||||
script,
|
||||
$arguments,
|
||||
);
|
||||
output = filter(proxies, targetPlatform);
|
||||
})();
|
||||
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) {
|
||||
// print log and skip this filter
|
||||
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
||||
}
|
||||
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) {
|
||||
// print log and skip this operator
|
||||
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// remove flag
|
||||
function removeFlag(str) {
|
||||
return str
|
||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function createDynamicFunction(name, script, $arguments) {
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
'$arguments',
|
||||
'$substore',
|
||||
'lodash',
|
||||
'$persistentStore',
|
||||
'$httpClient',
|
||||
'$notification',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
`${script}\n return ${name}`,
|
||||
)(
|
||||
$arguments,
|
||||
$,
|
||||
lodash,
|
||||
// eslint-disable-next-line no-undef
|
||||
$persistentStore,
|
||||
// eslint-disable-next-line no-undef
|
||||
$httpClient,
|
||||
// eslint-disable-next-line no-undef
|
||||
$notification,
|
||||
ProxyUtils,
|
||||
scriptResourceCache,
|
||||
);
|
||||
} else {
|
||||
return new Function(
|
||||
'$arguments',
|
||||
'$substore',
|
||||
'lodash',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
`${script}\n return ${name}`,
|
||||
)($arguments, $, lodash, ProxyUtils, scriptResourceCache);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function Clash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
// filter unsupported proxies
|
||||
proxies = proxies.filter((proxy) => {
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
return false;
|
||||
} else if (
|
||||
proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.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';
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
delete proxy['tls-fingerprint'];
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import Surge_Producer from './surge';
|
||||
import Clash_Producer from './clash';
|
||||
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';
|
||||
|
||||
function JSON_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => JSON.stringify(proxies, null, 2);
|
||||
return { type, produce };
|
||||
}
|
||||
|
||||
export default {
|
||||
QX: QX_Producer(),
|
||||
Surge: Surge_Producer(),
|
||||
Loon: Loon_Producer(),
|
||||
Clash: Clash_Producer(),
|
||||
URI: URI_Producer(),
|
||||
V2Ray: V2Ray_Producer(),
|
||||
JSON: JSON_Producer(),
|
||||
Stash: Stash_Producer(),
|
||||
};
|
||||
@@ -1,270 +0,0 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
const targetPlatform = 'Loon';
|
||||
import { isPresent, Result } from './utils';
|
||||
|
||||
export default function Loon_Producer() {
|
||||
const produce = (proxy) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
case 'trojan':
|
||||
return trojan(proxy);
|
||||
case 'vmess':
|
||||
return vmess(proxy);
|
||||
case 'vless':
|
||||
return vless(proxy);
|
||||
case 'http':
|
||||
return http(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}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
||||
);
|
||||
|
||||
// obfs
|
||||
if (isPresent(proxy, 'plugin')) {
|
||||
if (proxy.plugin === 'obfs') {
|
||||
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 {
|
||||
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
|
||||
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');
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function trojan(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
|
||||
);
|
||||
|
||||
// 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');
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function vmess(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
||||
);
|
||||
|
||||
// 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',
|
||||
);
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
result.append(`,alterId=0`);
|
||||
} else {
|
||||
result.append(`,alterId=${proxy.alterId}`);
|
||||
}
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function vless(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
||||
);
|
||||
|
||||
// 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',
|
||||
);
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${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');
|
||||
|
||||
// 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');
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
return result.toString();
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
import { isPresent, Result } from './utils';
|
||||
|
||||
const targetPlatform = 'QX';
|
||||
|
||||
export default function QX_Producer() {
|
||||
const produce = (proxy) => {
|
||||
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);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocks(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}`);
|
||||
|
||||
// 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)) {
|
||||
// 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');
|
||||
|
||||
// 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');
|
||||
|
||||
// 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 {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
// over tls
|
||||
if (proxy.network !== 'ws' && needTls(proxy)) {
|
||||
append(`,over-tls=true`);
|
||||
}
|
||||
|
||||
if (needTls(proxy)) {
|
||||
// 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');
|
||||
|
||||
// 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 {
|
||||
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)) {
|
||||
// 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');
|
||||
|
||||
// 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)) {
|
||||
// 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');
|
||||
|
||||
// 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)) {
|
||||
// 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');
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function needTls(proxy) {
|
||||
return proxy.tls;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function Stash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.filter((proxy) => {
|
||||
if (
|
||||
proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4'
|
||||
) {
|
||||
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'];
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
delete proxy['tls-fingerprint'];
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
import { Result, isPresent } from './utils';
|
||||
import { isNotBlank } 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) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
case 'trojan':
|
||||
return trojan(proxy);
|
||||
case 'vmess':
|
||||
return vmess(proxy);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'snell':
|
||||
return snell(proxy);
|
||||
case 'tuic':
|
||||
return tuic(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}`);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
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 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');
|
||||
|
||||
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 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');
|
||||
|
||||
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 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');
|
||||
|
||||
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 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');
|
||||
|
||||
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');
|
||||
|
||||
// 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',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// 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',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
// 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['fast-open']}`, 'fast-open');
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
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 {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
export default function URI_Producer() {
|
||||
const type = 'SINGLE';
|
||||
const produce = (proxy) => {
|
||||
let result = '';
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
||||
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
|
||||
proxy.port
|
||||
}/`;
|
||||
if (proxy.plugin) {
|
||||
result += '?plugin=';
|
||||
const opts = proxy['plugin-opts'];
|
||||
switch (proxy.plugin) {
|
||||
case 'obfs':
|
||||
result += encodeURIComponent(
|
||||
`simple-obfs;obfs=${opts.mode}${
|
||||
opts.host ? ';obfs-host=' + opts.host : ''
|
||||
}`,
|
||||
);
|
||||
break;
|
||||
case 'v2ray-plugin':
|
||||
result += encodeURIComponent(
|
||||
`v2ray-plugin;obfs=${opts.mode}${
|
||||
opts.host ? ';obfs-host' + opts.host : ''
|
||||
}${opts.tls ? ';tls' : ''}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported plugin option: ${proxy.plugin}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
result += `#${encodeURIComponent(proxy.name)}`;
|
||||
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
|
||||
result = {
|
||||
ps: proxy.name,
|
||||
add: proxy.server,
|
||||
port: proxy.port,
|
||||
id: proxy.uuid,
|
||||
type: '',
|
||||
aid: 0,
|
||||
net: proxy.network || 'tcp',
|
||||
tls: proxy.tls ? 'tls' : '',
|
||||
};
|
||||
if (proxy.tls && proxy.sni) {
|
||||
result.sni = proxy.sni;
|
||||
}
|
||||
// obfs
|
||||
if (proxy.network === 'ws') {
|
||||
result.path = proxy['ws-opts'].path || '/';
|
||||
if (proxy['ws-opts'].headers.Host) {
|
||||
result.host = proxy['ws-opts'].headers.Host;
|
||||
}
|
||||
}
|
||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||
break;
|
||||
case 'trojan':
|
||||
let transport = '';
|
||||
if (proxy.network) {
|
||||
transport = `&type=${proxy.network}`;
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let transportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (transportPath) {
|
||||
transport += `&path=${encodeURIComponent(
|
||||
Array.isArray(transportPath)
|
||||
? transportPath[0]
|
||||
: transportPath,
|
||||
)}`;
|
||||
}
|
||||
if (transportHost) {
|
||||
transport += `&host=${encodeURIComponent(
|
||||
Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
result = `trojan://${proxy.password}@${proxy.server}:${
|
||||
proxy.port
|
||||
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
|
||||
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
|
||||
}${transport}#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
import URI_Producer from './uri';
|
||||
|
||||
const URI = URI_Producer();
|
||||
|
||||
export default function V2Ray_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) =>
|
||||
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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 };
|
||||
})();
|
||||
@@ -1,58 +0,0 @@
|
||||
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)$/],
|
||||
];
|
||||
|
||||
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 (
|
||||
rule.type === 'IP-CIDR' ||
|
||||
rule.type === 'IP-CIDR6'
|
||||
) {
|
||||
rule.options = params.slice(2);
|
||||
}
|
||||
result.push(rule);
|
||||
}
|
||||
}
|
||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [AllRuleParser()];
|
||||
@@ -1,18 +0,0 @@
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||
// simply discard HTML
|
||||
const parse = () => '';
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function ClashProvider() {
|
||||
const name = 'Clash Provider';
|
||||
const test = (raw) => raw.indexOf('payload:') === 0;
|
||||
const parse = (raw) => {
|
||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [HTML(), ClashProvider()];
|
||||
@@ -1,81 +0,0 @@
|
||||
import YAML from 'static-js-yaml';
|
||||
|
||||
function QXFilter() {
|
||||
const type = 'SINGLE';
|
||||
const func = (rule) => {
|
||||
// skip unsupported rules
|
||||
const UNSUPPORTED = [
|
||||
'URL-REGEX',
|
||||
'DEST-PORT',
|
||||
'SRC-IP',
|
||||
'IN-PORT',
|
||||
'PROTOCOL',
|
||||
];
|
||||
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) => {
|
||||
let output = `${rule.type},${rule.content}`;
|
||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
||||
output += rule.options ? `,${rule.options[0]}` : '';
|
||||
}
|
||||
return output;
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
function LoonRules() {
|
||||
const type = 'SINGLE';
|
||||
const func = (rule) => {
|
||||
// skip unsupported rules
|
||||
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
|
||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||
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 (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
||||
output += rule.options ? `,${rule.options[0]}` : '';
|
||||
}
|
||||
return output;
|
||||
}),
|
||||
};
|
||||
return YAML.dump(conf);
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
export default {
|
||||
QX: QXFilter(),
|
||||
Surge: SurgeRuleSet(),
|
||||
Loon: LoonRules(),
|
||||
Clash: ClashRuleProvider(),
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
|
||||
* ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
|
||||
* ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
|
||||
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
||||
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
||||
* @author: Peng-YM
|
||||
* @github: https://github.com/Peng-YM/Sub-Store
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
import { version } from '../package.json';
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
import migrate from '@/utils/migration';
|
||||
import serve from '@/restful';
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
@@ -1,70 +0,0 @@
|
||||
import { version } from '../../package.json';
|
||||
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { syncToGist } from '@/restful/artifacts';
|
||||
|
||||
!(async function () {
|
||||
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 doSync() {
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store Sync -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const files = {};
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
allArtifacts.map(async (artifact) => {
|
||||
if (artifact.sync) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
|
||||
files[artifact.name] = {
|
||||
content: output,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const resp = await syncToGist(files);
|
||||
const body = JSON.parse(resp.body);
|
||||
|
||||
for (const artifact of allArtifacts) {
|
||||
if (artifact.sync) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
artifact.url = body.files[artifact.name].raw_url.replace(
|
||||
/\/raw\/[^/]*\/(.*)/,
|
||||
'/raw/$1',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
$.notify('🌍 Sub-Store', '全部订阅同步成功!');
|
||||
} catch (err) {
|
||||
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`);
|
||||
$.error(`无法同步订阅配置到 Gist,原因:${err}`);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { RuleUtils } from '@/core/rule-utils';
|
||||
import { version } from '../../package.json';
|
||||
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
const RESOURCE_TYPE = {
|
||||
PROXY: 1,
|
||||
RULE: 2,
|
||||
};
|
||||
|
||||
let result = $resource;
|
||||
|
||||
if ($resourceType === RESOURCE_TYPE.PROXY) {
|
||||
const proxies = ProxyUtils.parse($resource);
|
||||
result = ProxyUtils.produce(proxies, 'Loon');
|
||||
} else if ($resourceType === RESOURCE_TYPE.RULE) {
|
||||
const rules = RuleUtils.parse($resource);
|
||||
result = RuleUtils.produce(rules, 'Loon');
|
||||
}
|
||||
|
||||
$done(result);
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 路由拆分 - 本文件只包含不涉及到解析器的 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';
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
|
||||
function serve() {
|
||||
const $app = express({ substore: $ });
|
||||
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerSortRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
|
||||
$app.start();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 路由拆分 - 本文件仅包含使用到解析器的 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();
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
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.route('/api/artifacts')
|
||||
.get(getAllArtifacts)
|
||||
.post(createArtifact)
|
||||
.put(replaceArtifact);
|
||||
|
||||
$app.route('/api/artifact/:name')
|
||||
.get(getArtifact)
|
||||
.patch(updateArtifact)
|
||||
.delete(deleteArtifact);
|
||||
}
|
||||
|
||||
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;
|
||||
name = decodeURIComponent(name);
|
||||
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) {
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
let oldName = req.params.name;
|
||||
oldName = decodeURIComponent(oldName);
|
||||
const artifact = findByName(allArtifacts, oldName);
|
||||
if (artifact) {
|
||||
$.info(`正在更新远程配置:${artifact.name}`);
|
||||
const newArtifact = {
|
||||
...artifact,
|
||||
...req.body,
|
||||
};
|
||||
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;
|
||||
name = decodeURIComponent(name);
|
||||
$.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: '',
|
||||
};
|
||||
// 当别的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 } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
return Promise.reject('未设置Gist Token!');
|
||||
}
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
});
|
||||
return manager.upload(files);
|
||||
}
|
||||
|
||||
export { syncToGist };
|
||||
@@ -1,120 +0,0 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
|
||||
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}`);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
if (findByName(allCols, collection.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
`Collection ${collection.name} already exists.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
allCols.push(collection);
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res, collection, 201);
|
||||
}
|
||||
|
||||
function getCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
if (collection) {
|
||||
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;
|
||||
name = decodeURIComponent(name);
|
||||
let collection = req.body;
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const oldCol = findByName(allCols, name);
|
||||
if (oldCol) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
$.write(allArtifacts, ARTIFACTS_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;
|
||||
name = decodeURIComponent(name);
|
||||
$.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);
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { getPlatformFromHeaders } from '@/utils/platform';
|
||||
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import $ from '@/core/app';
|
||||
import { failed } from '@/restful/response';
|
||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
|
||||
export default function register($app) {
|
||||
$app.get('/download/collection/:name', downloadCollection);
|
||||
$app.get('/download/:name', downloadSubscription);
|
||||
}
|
||||
|
||||
async function downloadSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
$.info(`正在下载订阅:${name}`);
|
||||
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
if (sub) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'subscription',
|
||||
name,
|
||||
platform,
|
||||
});
|
||||
|
||||
if (sub.source !== 'local') {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'JSON') {
|
||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 下载订阅失败`,
|
||||
`❌ 无法下载订阅:${name}!`,
|
||||
`🤔 原因:${JSON.stringify(err)}`,
|
||||
);
|
||||
$.error(JSON.stringify(err));
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download subscription: ${name}`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Subscription ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
|
||||
$.info(`正在下载组合订阅:${name}`);
|
||||
|
||||
if (collection) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'collection',
|
||||
name,
|
||||
platform,
|
||||
});
|
||||
|
||||
// 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') {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'JSON') {
|
||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 下载组合订阅失败`,
|
||||
`❌ 下载组合订阅错误:${name}!`,
|
||||
`🤔 原因:${err}`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download collection: ${name}`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 下载组合订阅失败`,
|
||||
`❌ 未找到组合订阅:${name}!`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Collection ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import express from '@/vendor/express';
|
||||
import $ from '@/core/app';
|
||||
|
||||
import registerSubscriptionRoutes from './subscriptions';
|
||||
import registerCollectionRoutes from './collections';
|
||||
import registerArtifactRoutes from './artifacts';
|
||||
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';
|
||||
|
||||
export default function serve() {
|
||||
const $app = express({ substore: $ });
|
||||
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerDownloadRoutes($app);
|
||||
registerPreviewRoutes($app);
|
||||
registerSortingRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerSyncRoutes($app);
|
||||
registerNodeInfoRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
|
||||
$app.start();
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import $ from '@/core/app';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { version as substoreVersion } from '../../package.json';
|
||||
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
|
||||
import resourceCache from '@/utils/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';
|
||||
|
||||
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.json($.read('#sub-store'));
|
||||
})
|
||||
.post((req, res) => {
|
||||
const data = req.body;
|
||||
$.write(JSON.stringify(data), '#sub-store');
|
||||
res.end();
|
||||
});
|
||||
|
||||
// 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) {
|
||||
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
|
||||
let backend = 'Node';
|
||||
if (isNode) backend = 'Node';
|
||||
if (isQX) backend = 'QX';
|
||||
if (isLoon) backend = 'Loon';
|
||||
if (isSurge) backend = 'Surge';
|
||||
if (isStash) backend = 'Stash';
|
||||
if (isShadowRocket) backend = 'ShadowRocket';
|
||||
|
||||
success(res, {
|
||||
backend,
|
||||
version: substoreVersion,
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh(_, res) {
|
||||
// 1. get GitHub avatar and artifact store
|
||||
await updateGitHubAvatar();
|
||||
await updateArtifactStore();
|
||||
|
||||
// 2. clear resource cache
|
||||
resourceCache.revokeAll();
|
||||
success(res);
|
||||
}
|
||||
|
||||
async function gistBackup(req, res) {
|
||||
const { action } = 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 {
|
||||
const gist = new Gist({
|
||||
token: gistToken,
|
||||
key: GIST_BACKUP_KEY,
|
||||
});
|
||||
try {
|
||||
let content;
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const updated = settings.syncTime;
|
||||
switch (action) {
|
||||
case 'upload':
|
||||
// update syncTime
|
||||
settings.syncTime = new Date().getTime();
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
content = $.read('#sub-store');
|
||||
if ($.env.isNode)
|
||||
content = JSON.stringify($.cache, null, ` `);
|
||||
$.info(`上传备份中...`);
|
||||
try {
|
||||
await gist.upload({
|
||||
[GIST_BACKUP_FILE_NAME]: { content },
|
||||
});
|
||||
} 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);
|
||||
// restore settings
|
||||
$.write(content, '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
content = JSON.parse(content);
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
// perform migration after restoring from gist
|
||||
migrate();
|
||||
break;
|
||||
}
|
||||
success(res);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'BACKUP_FAILED',
|
||||
`Failed to ${action} data to gist!`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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,
|
||||
)}?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}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { InternalServerError, NetworkError } 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);
|
||||
}
|
||||
|
||||
async function compareSub(req, res) {
|
||||
const sub = req.body;
|
||||
const target = req.query.target || 'JSON';
|
||||
let content;
|
||||
if (sub.source === 'local') {
|
||||
content = sub.content;
|
||||
} else {
|
||||
try {
|
||||
content = await download(sub.url, sub.ua);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new NetworkError(
|
||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
||||
'无法下载远程资源',
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
const original = ProxyUtils.parse(content);
|
||||
|
||||
// add id
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
sub.process || [],
|
||||
target,
|
||||
);
|
||||
|
||||
// produce
|
||||
success(res, { original, processed });
|
||||
}
|
||||
|
||||
async function compareCollection(req, res) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const collection = req.body;
|
||||
const subnames = collection.subscriptions;
|
||||
const results = {};
|
||||
|
||||
await Promise.all(
|
||||
subnames.map(async (name) => {
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
let raw;
|
||||
if (sub.source === 'local') {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await download(sub.url, sub.ua);
|
||||
}
|
||||
// parse proxies
|
||||
let currentProxies = ProxyUtils.parse(raw);
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
'JSON',
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'PROCESS_FAILED',
|
||||
`处理子订阅 ${name} 失败`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// merge proxies with the original order
|
||||
const original = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
});
|
||||
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
collection.process || [],
|
||||
'JSON',
|
||||
);
|
||||
|
||||
success(res, { original, processed });
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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: error.details,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
|
||||
import { success } from './response';
|
||||
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) {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
if (!settings.avatarUrl) await updateGitHubAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
success(res, settings);
|
||||
}
|
||||
|
||||
async function updateSettings(req, res) {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const newSettings = {
|
||||
...settings,
|
||||
...req.body,
|
||||
};
|
||||
$.write(newSettings, SETTINGS_KEY);
|
||||
await updateGitHubAvatar();
|
||||
await updateArtifactStore();
|
||||
success(res, newSettings);
|
||||
}
|
||||
|
||||
export async function updateGitHubAvatar() {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const username = settings.githubUser;
|
||||
if (username) {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://api.github.com/users/${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 (e) {
|
||||
$.error('Failed to fetch GitHub avatar for User: ' + username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateArtifactStore() {
|
||||
$.log('Updating artifact store');
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const { githubUser, gistToken } = settings;
|
||||
if (githubUser && gistToken) {
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
});
|
||||
|
||||
try {
|
||||
const gistId = await manager.locate();
|
||||
if (gistId !== -1) {
|
||||
settings.artifactStore = `https://gist.github.com/${githubUser}/${gistId}`;
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error('Failed to fetch artifact store for User: ' + githubUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_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);
|
||||
}
|
||||
|
||||
function sortSubs(req, res) {
|
||||
const orders = req.body;
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
||||
$.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) - orders.indexOf(b));
|
||||
$.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) - orders.indexOf(b));
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
import {
|
||||
NetworkError,
|
||||
InternalServerError,
|
||||
ResourceNotFoundError,
|
||||
RequestInvalidError,
|
||||
} from './errors';
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import { success, failed } from './response';
|
||||
import $ from '@/core/app';
|
||||
|
||||
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;
|
||||
name = decodeURIComponent(name);
|
||||
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') {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'NO_FLOW_INFO',
|
||||
'N/A',
|
||||
`Local subscription ${name} has no flow information!`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const flowHeaders = await getFlowHeaders(sub.url);
|
||||
if (!flowHeaders) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'NO_FLOW_INFO',
|
||||
'No flow info',
|
||||
`Failed to fetch flow headers`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// unit is KB
|
||||
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
|
||||
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
||||
|
||||
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
|
||||
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
||||
|
||||
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
|
||||
|
||||
// optional expire timestamp
|
||||
const match = flowHeaders.match(/expire=(\d+)/);
|
||||
const expires = match ? Number(match[1]) : undefined;
|
||||
|
||||
success(res, { expires, total, usage: { upload, download } });
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new NetworkError(
|
||||
`URL_NOT_ACCESSIBLE`,
|
||||
`The URL for subscription ${name} is inaccessible.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createSubscription(req, res) {
|
||||
const sub = req.body;
|
||||
$.info(`正在创建订阅: ${sub.name}`);
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
if (findByName(allSubs, sub.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
`Subscription ${sub.name} already exists.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
allSubs.push(sub);
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res, sub, 201);
|
||||
}
|
||||
|
||||
function getSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
if (sub) {
|
||||
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;
|
||||
name = decodeURIComponent(name); // the original name
|
||||
let sub = req.body;
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const oldSub = findByName(allSubs, name);
|
||||
if (oldSub) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
$.write(allArtifacts, ARTIFACTS_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;
|
||||
name = decodeURIComponent(name);
|
||||
$.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);
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
ARTIFACTS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
RULES_KEY,
|
||||
SUBS_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 }) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
// produce Clash node format for ShadowRocket
|
||||
if (platform === 'ShadowRocket') platform = 'Clash';
|
||||
|
||||
if (type === 'subscription') {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
let raw;
|
||||
if (sub.source === 'local') {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await download(sub.url, sub.ua);
|
||||
}
|
||||
// parse proxies
|
||||
let proxies = ProxyUtils.parse(raw);
|
||||
// apply processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
sub.process || [],
|
||||
platform,
|
||||
);
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
if (exist[proxy.name]) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
'⚠️ 订阅包含重复节点!',
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'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);
|
||||
} else if (type === 'collection') {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
const subnames = collection.subscriptions;
|
||||
const results = {};
|
||||
let processed = 0;
|
||||
|
||||
await Promise.all(
|
||||
subnames.map(async (name) => {
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
$.info(`正在处理子订阅:${sub.name}...`);
|
||||
let raw;
|
||||
if (sub.source === 'local') {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await download(sub.url, sub.ua);
|
||||
}
|
||||
// parse proxies
|
||||
let currentProxies = ProxyUtils.parse(raw);
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
platform,
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
processed++;
|
||||
$.info(
|
||||
`✅ 子订阅:${sub.name}加载成功,进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}% `,
|
||||
);
|
||||
} catch (err) {
|
||||
processed++;
|
||||
$.error(
|
||||
`❌ 处理组合订阅中的子订阅: ${
|
||||
sub.name
|
||||
}时出现错误:${err},该订阅已被跳过!进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}%`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// merge proxies with the original order
|
||||
let proxies = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name]),
|
||||
);
|
||||
|
||||
// apply own processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
collection.process || [],
|
||||
platform,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`组合订阅中不含有效节点!`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
if (exist[proxy.name]) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
'⚠️ 订阅包含重复节点!',
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'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);
|
||||
} else if (type === 'rule') {
|
||||
const allRules = $.read(RULES_KEY);
|
||||
const rule = findByName(allRules, 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);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncAllArtifacts(_, res) {
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const files = {};
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
allArtifacts.map(async (artifact) => {
|
||||
if (artifact.sync) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
|
||||
files[artifact.name] = {
|
||||
content: output,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const resp = await syncToGist(files);
|
||||
const body = JSON.parse(resp.body);
|
||||
|
||||
for (const artifact of allArtifacts) {
|
||||
if (artifact.sync) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
artifact.url = body.files[artifact.name].raw_url.replace(
|
||||
/\/raw\/[^/]*\/(.*)/,
|
||||
'/raw/$1',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
$.info('全部订阅同步成功!');
|
||||
success(res);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_SYNC_ARTIFACTS`,
|
||||
`Failed to sync all artifacts`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
$.info(`同步订阅失败,原因:${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const artifact = findByName(allArtifacts, name);
|
||||
|
||||
if (!artifact) {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Artifact ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
|
||||
$.info(
|
||||
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
|
||||
artifact,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
try {
|
||||
const resp = await syncToGist({
|
||||
[encodeURIComponent(artifact.name)]: {
|
||||
content: output,
|
||||
},
|
||||
});
|
||||
artifact.updated = new Date().getTime();
|
||||
const body = JSON.parse(resp.body);
|
||||
artifact.url = body.files[
|
||||
encodeURIComponent(artifact.name)
|
||||
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, artifact);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_SYNC_ARTIFACT`,
|
||||
`Failed to sync artifact ${name}`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { produceArtifact };
|
||||
@@ -1,144 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,749 +0,0 @@
|
||||
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();
|
||||
@@ -1,17 +0,0 @@
|
||||
export function findByName(list, name) {
|
||||
return list.find((item) => item.name === name);
|
||||
}
|
||||
|
||||
export function findIndexByName(list, name) {
|
||||
return list.findIndex((item) => item.name === name);
|
||||
}
|
||||
|
||||
export function deleteByName(list, name) {
|
||||
const idx = findIndexByName(list, name);
|
||||
list.splice(idx, 1);
|
||||
}
|
||||
|
||||
export function updateByName(list, name, newItem) {
|
||||
const idx = findIndexByName(list, name);
|
||||
list[idx] = newItem;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
|
||||
const tasks = new Map();
|
||||
|
||||
export default async function download(url, ua) {
|
||||
const { isNode } = ENV();
|
||||
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
||||
const id = hex_md5(ua + url);
|
||||
if (!isNode && tasks.has(id)) {
|
||||
return tasks.get(id);
|
||||
}
|
||||
|
||||
const http = HTTP({
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
},
|
||||
});
|
||||
|
||||
const result = new Promise((resolve, reject) => {
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) {
|
||||
resolve(cached);
|
||||
} else {
|
||||
http.get(url)
|
||||
.then((resp) => {
|
||||
const body = resp.body;
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('远程资源内容为空!'));
|
||||
else {
|
||||
resourceCache.set(id, body);
|
||||
resolve(body);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error(`无法下载 URL:${url}`));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!isNode) {
|
||||
tasks.set(id, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { HTTP } from '@/vendor/open-api';
|
||||
|
||||
export async function getFlowHeaders(url) {
|
||||
const http = HTTP();
|
||||
const { headers } = await http.get({
|
||||
url,
|
||||
headers: {
|
||||
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
||||
},
|
||||
});
|
||||
const subkey = Object.keys(headers).filter((k) =>
|
||||
/SUBSCRIPTION-USERINFO/i.test(k),
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
// 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'],
|
||||
'🇸🇱': ['应急', '测试节点'],
|
||||
'🇦🇩': ['Andorra', '安道尔'],
|
||||
'🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜'],
|
||||
'🇦🇫': ['Afghanistan', '阿富汗'],
|
||||
'🇦🇱': ['Albania', '阿尔巴尼亚', '阿爾巴尼亞'],
|
||||
'🇦🇲': ['Armenia', '亚美尼亚'],
|
||||
'🇦🇷': ['Argentina', '阿根廷'],
|
||||
'🇦🇹': ['Austria', '奥地利', '奧地利', '维也纳'],
|
||||
'🇦🇺': [
|
||||
'Australia',
|
||||
'澳大利亚',
|
||||
'澳洲',
|
||||
'墨尔本',
|
||||
'悉尼',
|
||||
'土澳',
|
||||
'京澳',
|
||||
'廣澳',
|
||||
'滬澳',
|
||||
'沪澳',
|
||||
'广澳',
|
||||
'Sydney',
|
||||
],
|
||||
'🇦🇿': ['Azerbaijan', '阿塞拜疆'],
|
||||
'🇧🇦': ['Bosnia and Herzegovina', '波黑共和国', '波黑'],
|
||||
'🇧🇩': ['Bangladesh', '孟加拉国', '孟加拉'],
|
||||
'🇧🇪': ['Belgium', '比利时', '比利時'],
|
||||
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
|
||||
'🇧🇭': ['Bahrain', '巴林'],
|
||||
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
|
||||
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
|
||||
'🇨🇦': [
|
||||
'Canada',
|
||||
'加拿大',
|
||||
'蒙特利尔',
|
||||
'温哥华',
|
||||
'楓葉',
|
||||
'枫叶',
|
||||
'滑铁卢',
|
||||
'多伦多',
|
||||
'Waterloo',
|
||||
],
|
||||
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
|
||||
'🇨🇱': ['Chile', '智利'],
|
||||
'🇨🇴': ['Colombia', '哥伦比亚'],
|
||||
'🇨🇷': ['Costa Rica', '哥斯达黎加'],
|
||||
'🇨🇾': ['Cyprus', '塞浦路斯'],
|
||||
'🇨🇿': ['Czechia', '捷克'],
|
||||
'🇩🇪': [
|
||||
'German',
|
||||
'德国',
|
||||
'德國',
|
||||
'京德',
|
||||
'滬德',
|
||||
'廣德',
|
||||
'沪德',
|
||||
'广德',
|
||||
'法兰克福',
|
||||
'Frankfurt',
|
||||
],
|
||||
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
|
||||
'🇪🇨': ['Ecuador', '厄瓜多尔'],
|
||||
'🇪🇪': ['Estonia', '爱沙尼亚'],
|
||||
'🇪🇬': ['Egypt', '埃及'],
|
||||
'🇪🇸': ['Spain', '西班牙'],
|
||||
'🇪🇺': ['European Union', '欧盟', '欧罗巴'],
|
||||
'🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],
|
||||
'🇫🇷': ['France', '法国', '法國', '巴黎'],
|
||||
'🇬🇧': [
|
||||
'Great Britain',
|
||||
'英国',
|
||||
'England',
|
||||
'United Kingdom',
|
||||
'伦敦',
|
||||
'英',
|
||||
'London',
|
||||
],
|
||||
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
|
||||
'🇬🇷': ['Greece', '希腊', '希臘'],
|
||||
'🇭🇰': [
|
||||
'Hongkong',
|
||||
'香港',
|
||||
'Hong Kong',
|
||||
'HongKong',
|
||||
'HONG KONG',
|
||||
'深港',
|
||||
'沪港',
|
||||
'呼港',
|
||||
'穗港',
|
||||
'京港',
|
||||
'港',
|
||||
],
|
||||
'🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'],
|
||||
'🇭🇺': ['Hungary', '匈牙利'],
|
||||
'🇯🇴': ['Jordan', '约旦'],
|
||||
'🇯🇵': [
|
||||
'Japan',
|
||||
'日本',
|
||||
'东京',
|
||||
'大阪',
|
||||
'埼玉',
|
||||
'沪日',
|
||||
'穗日',
|
||||
'川日',
|
||||
'中日',
|
||||
'泉日',
|
||||
'杭日',
|
||||
'深日',
|
||||
'辽日',
|
||||
'广日',
|
||||
'大坂',
|
||||
'Osaka',
|
||||
'Tokyo',
|
||||
],
|
||||
'🇰🇪': ['Kenya', '肯尼亚'],
|
||||
'🇰🇬': ['Kyrgyzstan', '吉尔吉斯斯坦'],
|
||||
'🇰🇭': ['Cambodia', '柬埔寨'],
|
||||
'🇰🇵': ['North Korea', '朝鲜'],
|
||||
'🇰🇷': [
|
||||
'Korea',
|
||||
'韩国',
|
||||
'韓國',
|
||||
'韩',
|
||||
'韓',
|
||||
'首尔',
|
||||
'春川',
|
||||
'Chuncheon',
|
||||
'Seoul',
|
||||
],
|
||||
'🇰🇿': ['Kazakhstan', '哈萨克斯坦', '哈萨克'],
|
||||
'🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],
|
||||
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
|
||||
'🇮🇱': ['Israel', '以色列'],
|
||||
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
|
||||
'🇮🇳': ['India', '印度', '孟买', 'MFumbai'],
|
||||
'🇮🇷': ['Iran', '伊朗'],
|
||||
'🇮🇸': ['Iceland', '冰岛', '冰島'],
|
||||
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
|
||||
'🇱🇹': ['Lithuania', '立陶宛'],
|
||||
'🇱🇺': ['Luxembourg', '卢森堡'],
|
||||
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
|
||||
'🇲🇦': ['Morocco', '摩洛哥'],
|
||||
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
|
||||
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
|
||||
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
|
||||
'🇲🇳': ['Mongolia', '蒙古'],
|
||||
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
|
||||
'🇲🇹': ['Malta', '马耳他'],
|
||||
'🇲🇽': ['Mexico', '墨西哥'],
|
||||
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
|
||||
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
|
||||
'🇳🇴': ['Norway', '挪威'],
|
||||
'🇳🇵': ['Nepal', '尼泊尔'],
|
||||
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
|
||||
'🇵🇦': ['Panama', '巴拿马'],
|
||||
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
|
||||
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
|
||||
'🇵🇰': ['Pakistan', '巴基斯坦'],
|
||||
'🇵🇱': ['Poland', '波兰', '波蘭'],
|
||||
'🇵🇷': ['Puerto Rico', '波多黎各'],
|
||||
'🇵🇹': ['Portugal', '葡萄牙'],
|
||||
'🇵🇾': ['Paraguay', '巴拉圭'],
|
||||
'🇷🇴': ['Romania', '罗马尼亚'],
|
||||
'🇷🇸': ['Serbia', '塞尔维亚'],
|
||||
'🇷🇪': ['Réunion', '留尼汪', '法属留尼汪'],
|
||||
'🇷🇺': [
|
||||
'Russia',
|
||||
'俄罗斯',
|
||||
'俄国',
|
||||
'俄羅斯',
|
||||
'伯力',
|
||||
'莫斯科',
|
||||
'圣彼得堡',
|
||||
'西伯利亚',
|
||||
'京俄',
|
||||
'杭俄',
|
||||
'廣俄',
|
||||
'滬俄',
|
||||
'广俄',
|
||||
'沪俄',
|
||||
'Moscow',
|
||||
],
|
||||
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'],
|
||||
'🇸🇪': ['Sweden', '瑞典'],
|
||||
'🇸🇬': [
|
||||
'Singapore',
|
||||
'新加坡',
|
||||
'狮城',
|
||||
'沪新',
|
||||
'京新',
|
||||
'中新',
|
||||
'泉新',
|
||||
'穗新',
|
||||
'深新',
|
||||
'杭新',
|
||||
'广新',
|
||||
'廣新',
|
||||
'滬新',
|
||||
],
|
||||
'🇸🇮': ['Slovenia', '斯洛文尼亚'],
|
||||
'🇸🇰': ['Slovakia', '斯洛伐克'],
|
||||
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
|
||||
'🇹🇳': ['Tunisia', '突尼斯'],
|
||||
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔'],
|
||||
'🇹🇼': [
|
||||
'Taiwan',
|
||||
'台湾',
|
||||
'台北',
|
||||
'台中',
|
||||
'新北',
|
||||
'彰化',
|
||||
'台',
|
||||
'Taipei',
|
||||
],
|
||||
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
|
||||
'🇺🇸': [
|
||||
'United States',
|
||||
'美国',
|
||||
'America',
|
||||
'美',
|
||||
'京美',
|
||||
'波特兰',
|
||||
'达拉斯',
|
||||
'俄勒冈',
|
||||
'凤凰城',
|
||||
'费利蒙',
|
||||
'硅谷',
|
||||
'矽谷',
|
||||
'拉斯维加斯',
|
||||
'洛杉矶',
|
||||
'圣何塞',
|
||||
'圣克拉拉',
|
||||
'西雅图',
|
||||
'芝加哥',
|
||||
'沪美',
|
||||
'哥伦布',
|
||||
'纽约',
|
||||
'Los Angeles',
|
||||
'San Jose',
|
||||
'Sillicon Valley',
|
||||
'Michigan',
|
||||
],
|
||||
'🇺🇾': ['Uruguay', '乌拉圭'],
|
||||
'🇻🇪': ['Venezuela', '委内瑞拉'],
|
||||
'🇻🇳': ['Vietnam', '越南', '胡志明'],
|
||||
'🇿🇦': ['South Africa', '南非'],
|
||||
'🇨🇳': [
|
||||
'China',
|
||||
'中国',
|
||||
'中國',
|
||||
'回国',
|
||||
'回國',
|
||||
'国内',
|
||||
'國內',
|
||||
'华东',
|
||||
'华西',
|
||||
'华南',
|
||||
'华北',
|
||||
'华中',
|
||||
'江苏',
|
||||
'北京',
|
||||
'上海',
|
||||
'广州',
|
||||
'深圳',
|
||||
'杭州',
|
||||
'徐州',
|
||||
'青岛',
|
||||
'宁波',
|
||||
'镇江',
|
||||
],
|
||||
};
|
||||
|
||||
const ISOFlags = {
|
||||
'🏳️🌈': ['EXP', 'BAND'],
|
||||
'🇸🇱': ['TEST', 'SOS'],
|
||||
'🇦🇩': ['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'],
|
||||
'🇧🇷': ['BR', 'BRA'],
|
||||
'🇧🇾': ['BY', 'BLR'],
|
||||
'🇨🇦': ['CA', 'CAN'],
|
||||
'🇨🇭': ['CH', 'CHE'],
|
||||
'🇨🇱': ['CL', 'CHL'],
|
||||
'🇨🇴': ['CO', 'COL'],
|
||||
'🇨🇷': ['CR', 'CRI'],
|
||||
'🇨🇾': ['CY', 'CYP'],
|
||||
'🇨🇿': ['CZ', 'CZE'],
|
||||
'🇩🇪': ['DE', 'DEU'],
|
||||
'🇩🇰': ['DK', 'DNK'],
|
||||
'🇪🇨': ['EC', 'ECU'],
|
||||
'🇪🇪': ['EE', 'EST'],
|
||||
'🇪🇬': ['EG', 'EGY'],
|
||||
'🇪🇸': ['ES', 'ESP'],
|
||||
'🇪🇺': ['EU'],
|
||||
'🇫🇮': ['FI', 'FIN'],
|
||||
'🇫🇷': ['FR', 'FRA'],
|
||||
'🇬🇧': ['GB', 'GBR', 'UK'],
|
||||
'🇬🇪': ['GE', 'GEO'],
|
||||
'🇬🇷': ['GR', 'GRC'],
|
||||
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
|
||||
'🇭🇷': ['HR', 'HRV'],
|
||||
'🇭🇺': ['HU', 'HUN'],
|
||||
'🇯🇴': ['JO', 'JOR'],
|
||||
'🇯🇵': ['JP', 'JPN'],
|
||||
'🇰🇪': ['KE', 'KEN'],
|
||||
'🇰🇬': ['KG', 'KGZ'],
|
||||
'🇰🇭': ['KH', 'KGZ'],
|
||||
'🇰🇵': ['KP', 'PRK'],
|
||||
'🇰🇷': ['KR', 'KOR'],
|
||||
'🇰🇿': ['KZ', 'KAZ'],
|
||||
'🇮🇩': ['ID', 'IDN'],
|
||||
'🇮🇪': ['IE', 'IRL'],
|
||||
'🇮🇱': ['IL', 'ISR'],
|
||||
'🇮🇲': ['IM', 'IMN'],
|
||||
'🇮🇳': ['IN', 'IND'],
|
||||
'🇮🇷': ['IR', 'IRN'],
|
||||
'🇮🇸': ['IS', 'ISL'],
|
||||
'🇮🇹': ['IT', 'ITA'],
|
||||
'🇱🇹': ['LT', 'LTU'],
|
||||
'🇱🇺': ['LU', 'LUX'],
|
||||
'🇱🇻': ['LV', 'LVA'],
|
||||
'🇲🇦': ['MA', 'MAR'],
|
||||
'🇲🇩': ['MD', 'MDA'],
|
||||
'🇳🇬': ['NG', 'NGA'],
|
||||
'🇲🇰': ['MK', 'MKD'],
|
||||
'🇲🇳': ['MN', 'MNG'],
|
||||
'🇲🇴': ['MO', 'MAC', 'CTM'],
|
||||
'🇲🇹': ['MT', 'MLT'],
|
||||
'🇲🇽': ['MX', 'MEX'],
|
||||
'🇲🇾': ['MY', 'MYS'],
|
||||
'🇳🇱': ['NL', 'NLD'],
|
||||
'🇳🇴': ['NO', 'NOR'],
|
||||
'🇳🇵': ['NP', 'NPL'],
|
||||
'🇳🇿': ['NZ', 'NZL'],
|
||||
'🇵🇦': ['PA', 'PAN'],
|
||||
'🇵🇪': ['PE', 'PER'],
|
||||
'🇵🇭': ['PH', 'PHL'],
|
||||
'🇵🇰': ['PK', 'PAK'],
|
||||
'🇵🇱': ['PL', 'POL'],
|
||||
'🇵🇷': ['PR', 'PRI'],
|
||||
'🇵🇹': ['PT', 'PRT'],
|
||||
'🇵🇾': ['PY', 'PRY'],
|
||||
'🇷🇴': ['RO', 'ROU'],
|
||||
'🇷🇸': ['RS', 'SRB'],
|
||||
'🇷🇪': ['RE', 'REU'],
|
||||
'🇷🇺': ['RU', 'RUS'],
|
||||
'🇸🇦': ['SA', 'SAU'],
|
||||
'🇸🇪': ['SE', 'SWE'],
|
||||
'🇸🇬': ['SG', 'SGP'],
|
||||
'🇸🇮': ['SI', 'SVN'],
|
||||
'🇸🇰': ['SK', 'SVK'],
|
||||
'🇹🇭': ['TH', 'THA'],
|
||||
'🇹🇳': ['TN', 'TUN'],
|
||||
'🇹🇷': ['TR', 'TUR'],
|
||||
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET'],
|
||||
'🇺🇦': ['UA', 'UKR'],
|
||||
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
|
||||
'🇺🇾': ['UY', 'URY'],
|
||||
'🇻🇪': ['VE', 'VEN'],
|
||||
'🇻🇳': ['VN', 'VNM'],
|
||||
'🇿🇦': ['ZA', 'ZAF'],
|
||||
'🇨🇳': ['CN', 'CHN', 'BACK'],
|
||||
};
|
||||
// 原旗帜或空
|
||||
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))
|
||||
) {
|
||||
//console.log(`newFlag = ${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),
|
||||
)
|
||||
) {
|
||||
//console.log(`ISOFlag = ${flag}`)
|
||||
return (Flag = flag);
|
||||
}
|
||||
}
|
||||
//console.log(`Final Flag = ${Flag}`)
|
||||
return Flag;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { HTTP } from '@/vendor/open-api';
|
||||
|
||||
/**
|
||||
* Gist backup
|
||||
*/
|
||||
export default class Gist {
|
||||
constructor({ token, key }) {
|
||||
this.http = HTTP({
|
||||
baseURL: 'https://api.github.com',
|
||||
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',
|
||||
},
|
||||
events: {
|
||||
onResponse: (resp) => {
|
||||
if (/^[45]/.test(String(resp.statusCode))) {
|
||||
return Promise.reject(
|
||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
async locate() {
|
||||
return this.http.get('/gists').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
for (let g of gists) {
|
||||
if (g.description === this.key) {
|
||||
return g.id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
|
||||
async upload(files) {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject('未提供需上传的文件');
|
||||
}
|
||||
|
||||
const id = await this.locate();
|
||||
|
||||
if (id === -1) {
|
||||
// create a new gist for backup
|
||||
return this.http.post({
|
||||
url: '/gists',
|
||||
body: JSON.stringify({
|
||||
description: this.key,
|
||||
public: false,
|
||||
files,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// update an existing gist
|
||||
return this.http.patch({
|
||||
url: `/gists/${id}`,
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async download(filename) {
|
||||
const id = await this.locate();
|
||||
if (id === -1) {
|
||||
return Promise.reject('未找到Gist备份!');
|
||||
} else {
|
||||
try {
|
||||
const { files } = await this.http
|
||||
.get(`/gists/${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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent };
|
||||
@@ -1,17 +0,0 @@
|
||||
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 };
|
||||
@@ -1,128 +0,0 @@
|
||||
import {
|
||||
SUBS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
SCHEMA_VERSION_KEY,
|
||||
ARTIFACTS_KEY,
|
||||
RULES_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. 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;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export function getPlatformFromHeaders(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let UA = '';
|
||||
for (let k of keys) {
|
||||
if (/USER-AGENT/i.test(k)) {
|
||||
UA = headers[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} 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 {
|
||||
return 'JSON';
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import $ from '@/core/app';
|
||||
import { CACHE_EXPIRATION_TIME_MS, RESOURCE_CACHE_KEY } from '@/constants';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache(CACHE_EXPIRATION_TIME_MS);
|
||||
@@ -1,105 +0,0 @@
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
SCRIPT_RESOURCE_CACHE_KEY,
|
||||
CSR_EXPIRATION_TIME_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
class ResourceCache {
|
||||
constructor() {
|
||||
this.expires = getExpiredTime();
|
||||
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this.resourceCache = JSON.parse($.read(SCRIPT_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), SCRIPT_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;
|
||||
}
|
||||
|
||||
gettime(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].time;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
function getExpiredTime() {
|
||||
// console.log($.read(CSR_EXPIRATION_TIME_KEY));
|
||||
if (!$.read(CSR_EXPIRATION_TIME_KEY)) {
|
||||
$.write('1728e5', CSR_EXPIRATION_TIME_KEY); // 48 * 3600 * 1000
|
||||
}
|
||||
let expiration = 1728e5;
|
||||
if ($.env.isLoon) {
|
||||
const loont = {
|
||||
// Loon 插件自义定
|
||||
'1\u5206\u949f': 6e4,
|
||||
'5\u5206\u949f': 3e5,
|
||||
'10\u5206\u949f': 6e5,
|
||||
'30\u5206\u949f': 18e5, // "30分钟"
|
||||
'1\u5c0f\u65f6': 36e5,
|
||||
'2\u5c0f\u65f6': 72e5,
|
||||
'3\u5c0f\u65f6': 108e5,
|
||||
'6\u5c0f\u65f6': 216e5,
|
||||
'12\u5c0f\u65f6': 432e5,
|
||||
'24\u5c0f\u65f6': 864e5,
|
||||
'48\u5c0f\u65f6': 1728e5,
|
||||
'72\u5c0f\u65f6': 2592e5, // "72小时"
|
||||
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
|
||||
};
|
||||
let intimed = $.read('#\u8282\u70b9\u7f13\u5b58\u6709\u6548\u671f'); // Loon #节点缓存有效期
|
||||
// console.log(intimed);
|
||||
if (intimed in loont) {
|
||||
expiration = loont[intimed];
|
||||
if (expiration === 'readcachets') {
|
||||
expiration = intimed;
|
||||
}
|
||||
}
|
||||
return expiration;
|
||||
} else {
|
||||
expiration = $.read(CSR_EXPIRATION_TIME_KEY);
|
||||
return expiration;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache();
|
||||
Vendored
-295
@@ -1,295 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { ENV } from './open-api';
|
||||
|
||||
export default function express({ substore: $, port }) {
|
||||
port = port || 3000;
|
||||
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',
|
||||
};
|
||||
|
||||
// node support
|
||||
if (isNode) {
|
||||
const express_ = eval(`require("express")`);
|
||||
const bodyParser = eval(`require("body-parser")`);
|
||||
const app = express_();
|
||||
app.use(bodyParser.json({ verify: rawBodySaver }));
|
||||
app.use(
|
||||
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
|
||||
);
|
||||
app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));
|
||||
app.use((_, res, next) => {
|
||||
res.set(DEFAULT_HEADERS);
|
||||
next();
|
||||
});
|
||||
|
||||
// adapter
|
||||
app.start = () => {
|
||||
app.listen(port, () => {
|
||||
$.info(`Express started on port: ${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 } = 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) {
|
||||
$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;
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
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]] = 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('')] = val.join('');
|
||||
} else {
|
||||
if (pattern[i] !== path[j]) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
Vendored
-387
@@ -1,387 +0,0 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
Vendored
-324
@@ -1,324 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
const isQX = typeof $task !== 'undefined';
|
||||
const isLoon = typeof $loon !== 'undefined';
|
||||
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;
|
||||
|
||||
export class OpenAPI {
|
||||
constructor(name = 'untitled', debug = false) {
|
||||
this.name = name;
|
||||
this.debug = debug;
|
||||
|
||||
this.http = HTTP();
|
||||
this.env = ENV();
|
||||
|
||||
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 (isNode) {
|
||||
// create a json for root cache
|
||||
let fpath = 'root.json';
|
||||
if (!this.node.fs.existsSync(fpath)) {
|
||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||
flag: 'wx',
|
||||
});
|
||||
this.root = {};
|
||||
} else {
|
||||
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
||||
}
|
||||
|
||||
// create a json file with the given name if not exists
|
||||
fpath = `${this.name}.json`;
|
||||
if (!this.node.fs.existsSync(fpath)) {
|
||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||
flag: 'wx',
|
||||
});
|
||||
this.cache = {};
|
||||
} else {
|
||||
this.cache = JSON.parse(
|
||||
this.node.fs.readFileSync(`${this.name}.json`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (isNode) {
|
||||
this.node.fs.writeFileSync(
|
||||
`${this.name}.json`,
|
||||
data,
|
||||
{ flag: 'w' },
|
||||
(err) => console.log(err),
|
||||
);
|
||||
this.node.fs.writeFileSync(
|
||||
'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;
|
||||
}
|
||||
} 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];
|
||||
}
|
||||
} 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];
|
||||
}
|
||||
} 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`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
$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 };
|
||||
}
|
||||
|
||||
export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
const { isQX, isLoon, isSurge, isNode } = 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,
|
||||
});
|
||||
} else if (isLoon || isSurge || isNode) {
|
||||
worker = new Promise((resolve, reject) => {
|
||||
const request = isNode
|
||||
? eval("require('request')")
|
||||
: $httpClient;
|
||||
request[method.toLowerCase()](
|
||||
options,
|
||||
(err, response, body) => {
|
||||
if (err) reject(err);
|
||||
else
|
||||
resolve({
|
||||
statusCode:
|
||||
response.status || response.statusCode,
|
||||
headers: response.headers,
|
||||
body,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let timeoutid;
|
||||
const timer = timeout
|
||||
? new Promise((_, reject) => {
|
||||
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) => {
|
||||
clearTimeout(timeoutid);
|
||||
return res;
|
||||
})
|
||||
: worker
|
||||
).then((resp) => events.onResponse(resp));
|
||||
}
|
||||
|
||||
const http = {};
|
||||
methods.forEach(
|
||||
(method) =>
|
||||
(http[method.toLowerCase()] = (options) => send(method, options)),
|
||||
);
|
||||
return http;
|
||||
}
|
||||
Vendored
-16
File diff suppressed because one or more lines are too long
@@ -1,19 +0,0 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具
|
||||
#!openUrl=https://sub.store
|
||||
#!author=Peng-YM
|
||||
#!homepage=https://github.com/Peng-YM/Sub-Store
|
||||
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
||||
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||
|
||||
[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 "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
|
||||
@@ -1,4 +0,0 @@
|
||||
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
|
||||
@@ -1,22 +0,0 @@
|
||||
# Sub-Store 配置指南
|
||||
|
||||
## 脚本配置:
|
||||
|
||||
### 1. Loon
|
||||
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。
|
||||
### 2. Surge
|
||||
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
|
||||
|
||||
### 3. QX
|
||||
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可
|
||||
|
||||
### 4. Stash
|
||||
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。
|
||||
|
||||
### 5. Shadowrocket
|
||||
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
|
||||
|
||||
## 使用 Sub-Store
|
||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
||||
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Sub-Store
|
||||
desc: 高级订阅管理工具 @Peng-YM
|
||||
|
||||
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: "0 0 * * *"
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
|
||||
[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
|
||||
|
||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
@@ -1,11 +0,0 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
|
||||
|
||||
[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
|
||||
|
||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
@@ -1,11 +0,0 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
|
||||
[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
|
||||
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
|
||||
|
||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
||||
Vendored
+1168
File diff suppressed because one or more lines are too long
@@ -1,40 +0,0 @@
|
||||
upstream api {
|
||||
server 0.0.0.0:3000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 6080;
|
||||
# allow 127.0.0.1;
|
||||
# allow 0.0.0.0;
|
||||
# deny all;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.0;
|
||||
|
||||
location / {
|
||||
root /Sub-Store/web/dist;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
}
|
||||
|
||||
location /download {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://api;
|
||||
}
|
||||
|
||||
}
|
||||
Generated
-40
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
|
||||
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* 节点名改为花里胡哨字体,仅支持英文字符和数字
|
||||
*
|
||||
* 【字体】
|
||||
* 可参考: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
|
||||
*
|
||||
* 【示例】
|
||||
* 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","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
|
||||
};
|
||||
|
||||
// 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; }
|
||||
@@ -1,175 +0,0 @@
|
||||
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 - _ }
|
||||
@@ -1,5 +0,0 @@
|
||||
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)}
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 为节点添加 tls 证书指纹
|
||||
* 示例
|
||||
* #fingerprint=...
|
||||
*/
|
||||
function operator(proxies) {
|
||||
const { fingerprint } = $arguments;
|
||||
proxies.forEach(proxy => {
|
||||
proxy['tls-fingerprint'] = fingerprint;
|
||||
});
|
||||
return proxies;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 过滤 UDP 节点
|
||||
*/
|
||||
function filter(proxies) {
|
||||
return proxies.map(p => p.udp);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 为 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;
|
||||
}
|
||||
Vendored
+1169
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user