mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
⛄ Sync 2026-03-22 12:50:34
This commit is contained in:
parent
e7f88458b2
commit
8d097e3078
@ -1,16 +1,16 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=hickory-dns
|
||||
PKG_VERSION:=2026.03.13
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=2026.03.22
|
||||
PKG_RELEASE:=4
|
||||
|
||||
HICKORY_COMMIT:=b6c4944a1884c82716ab41690ce48a3b828618ba
|
||||
HICKORY_COMMIT:=32a162faa247b6873ab86e6b1605f28154aa5e7a
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/b6c4944a1884c82716ab41690ce48a3b828618ba?
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/32a162faa247b6873ab86e6b1605f28154aa5e7a?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-b6c4944a1884c82716ab41690ce48a3b828618ba
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-32a162faa247b6873ab86e6b1605f28154aa5e7a
|
||||
|
||||
PKG_BUILD_DEPENDS:=rust/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is 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. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
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.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
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 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. Use with the GNU Affero General Public License.
|
||||
|
||||
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 Affero 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 special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU 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 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 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 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 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
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 GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@ -1,28 +1,18 @@
|
||||
# SPDX-License-Identifier: GPL-3.0
|
||||
#
|
||||
# Copyright (C) 2022-2025 muink <https://github.com/muink>
|
||||
# Copyright (C) 2021-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-netdata
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_NAME:=luci-app-netdata
|
||||
|
||||
LUCI_TITLE:=LuCI Netdata
|
||||
LUCI_DEPENDS:=+netdata-ssl +openssl-util
|
||||
LUCI_TITLE:=LuCI app for netdata
|
||||
LUCI_DEPENDS:=+netdata
|
||||
|
||||
LUCI_DESCRIPTION:=Real-time performance monitoring
|
||||
|
||||
PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
|
||||
PKG_NAME:=luci-app-netdata
|
||||
PKG_VERSION:=2.0.4
|
||||
PKG_RELEASE:=2
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
define Package/$(LUCI_NAME)/conffiles
|
||||
endef
|
||||
|
||||
define Package/$(LUCI_NAME)/postinst
|
||||
endef
|
||||
|
||||
define Package/$(LUCI_NAME)/prerm
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
# LuCI Netdata
|
||||
> [Netdata][] is high-fidelity infrastructure monitoring and troubleshooting.
|
||||
Open-source, free, preconfigured, opinionated, and always real-time.
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
### How to install
|
||||
|
||||
1. Goto ~~[releases](https://github.com/muink/luci-app-netdata/tree/releases)~~ [here](https://fantastic-packages.github.io/packages/)
|
||||
2. Download the latest version of ipk
|
||||
3. Login router and goto **System --> Software**
|
||||
4. Upload and install ipk
|
||||
5. Reboot if the app is not automatically added in page
|
||||
6. Goto **Status --> Netdata**
|
||||
7. If after program update. LuCI is not working properly, please reinstall luci-app-netdata.
|
||||
8. After you upgrade Netdata, you need to reinstall netdata and luci-app-netdata.
|
||||
|
||||
### Build
|
||||
|
||||
- Compile from OpenWrt/LEDE SDK
|
||||
|
||||
```
|
||||
# Take the x86_64 platform as an example
|
||||
tar xjf openwrt-sdk-21.02.3-x86-64_gcc-8.4.0_musl.Linux-x86_64.tar.xz
|
||||
# Go to the SDK root dir
|
||||
cd OpenWrt-sdk-*-x86_64_*
|
||||
# First run to generate a .config file
|
||||
make menuconfig
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
# Get Makefile
|
||||
git clone --depth 1 --branch master --single-branch --no-checkout https://github.com/muink/luci-app-netdata.git package/luci-app-netdata
|
||||
pushd package/luci-app-netdata
|
||||
umask 022
|
||||
git checkout
|
||||
popd
|
||||
# Select the package LuCI -> Applications -> luci-app-netdata
|
||||
make menuconfig
|
||||
# Start compiling
|
||||
make package/luci-app-netdata/compile V=99
|
||||
```
|
||||
|
||||
[Netdata]: https://github.com/netdata/netdata
|
||||
|
||||
### License
|
||||
|
||||
- This project is licensed under the [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
const isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load() {
|
||||
return L.resolveDefault(fs.read('/etc/netdata/netdata.conf'), '');
|
||||
},
|
||||
|
||||
handleSave(ev) {
|
||||
let value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n';
|
||||
|
||||
return fs.write('/etc/netdata/netdata.conf', value).then((rc) => {
|
||||
document.querySelector('textarea').value = value;
|
||||
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
|
||||
|
||||
return fs.exec('/etc/init.d/netdata', [ 'reload' ]);
|
||||
}).catch((e) => {
|
||||
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)));
|
||||
});
|
||||
},
|
||||
|
||||
render(conf) {
|
||||
return E([
|
||||
E('h4', _('Edit Netdata main config: <code>/etc/netdata/netdata.conf</code>')),
|
||||
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 25, 'disabled': isReadonlyView }, [ conf != null ? conf : '' ]))
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,183 @@
|
||||
// Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netdata
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require fs';
|
||||
const getNetdataVersion = rpc.declare({
|
||||
object: 'luci.netdata',
|
||||
method: 'get_version',
|
||||
expect: { 'version': '' }
|
||||
});
|
||||
async function checkProcess() {
|
||||
try {
|
||||
const pidofRes = await fs.exec('/bin/pidof', ['netdata']);
|
||||
if (pidofRes.code === 0) {
|
||||
return {
|
||||
running: true,
|
||||
pid: pidofRes.stdout.trim()
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore error
|
||||
}
|
||||
try {
|
||||
const psRes = await fs.exec('/bin/ps', ['-C', 'netdata', '-o', 'pid=']);
|
||||
const pid = psRes.stdout.trim();
|
||||
return {
|
||||
running: pid !== '',
|
||||
pid: pid || null
|
||||
};
|
||||
} catch (err) {
|
||||
return { running: false, pid: null };
|
||||
}
|
||||
}
|
||||
function getVersionInfo() {
|
||||
return L.resolveDefault(getNetdataVersion(), {}).then(function(result) {
|
||||
return result || {};
|
||||
}).catch(function(error) {
|
||||
console.error('Failed to get version:', error);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(checkProcess(), {}).then(function(res) {
|
||||
let isRunning = false;
|
||||
try {
|
||||
if (res && res.running) {
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Service status error:', e);
|
||||
}
|
||||
return isRunning;
|
||||
}).catch(function(error) {
|
||||
console.error('Service status check failed:', error);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning, webport, protocol, version) {
|
||||
let statusText = isRunning ? _('RUNNING') : _('NOT RUNNING');
|
||||
let color = isRunning ? 'green' : 'red';
|
||||
let icon = isRunning ? '✓' : '✗';
|
||||
let versionText = version ? `${version}` : '';
|
||||
let html = String.format(
|
||||
'<em><span style="color:%s">%s <strong>%s %s - %s</strong></span></em>',
|
||||
color, icon, _('Netdata'),versionText, statusText
|
||||
);
|
||||
|
||||
if (isRunning) {
|
||||
let buttonUrl = String.format('%s//%s:%s/', protocol, window.location.hostname, webport);
|
||||
|
||||
html += String.format(
|
||||
'<input class="cbi-button cbi-button-reload" type="button" style="margin-left: 20px" value="%s" onclick="window.open(\'%s\')">',
|
||||
_('Open Web UI'),
|
||||
buttonUrl
|
||||
);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('netdata')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
let m, s, o;
|
||||
let webport = uci.get_first('netdata', 'netdata', 'port') || '19999';
|
||||
let uci_ssl = uci.get_first('netdata', 'netdata', 'enable_ssl') || '0';
|
||||
let protocol = uci_ssl === '1' ? 'https:' : 'http:';
|
||||
|
||||
m = new form.Map('netdata', _('Netdata'),
|
||||
_('An open-source real-time performance monitoring and visualization tool'));
|
||||
|
||||
// 状态显示部分
|
||||
s = m.section(form.TypedSection);
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
s.render = function() {
|
||||
poll.add(function() {
|
||||
return Promise.all([
|
||||
getServiceStatus(),
|
||||
getVersionInfo()
|
||||
]).then(function(results) {
|
||||
var [isRunning, version] = results;
|
||||
var view = document.getElementById('service_status');
|
||||
if (view) {
|
||||
view.innerHTML = renderStatus(isRunning, webport, protocol, version);
|
||||
}
|
||||
}).catch(function(error) {
|
||||
console.error('Poll error:', error);
|
||||
});
|
||||
}, 5); // 添加轮询间隔5秒
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('div', { id: 'service_status' },
|
||||
E('p', {}, _('Collecting data...'))
|
||||
),
|
||||
E('div', { 'style': 'text-align: right; font-style: italic;' }, [
|
||||
E('span', {}, [
|
||||
_('© github '),
|
||||
E('a', {
|
||||
'href': 'https://github.com/sirpdboy',
|
||||
'target': '_blank',
|
||||
'style': 'text-decoration: none;'
|
||||
}, 'by sirpdboy')
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// 配置部分
|
||||
s = m.section(form.NamedSection, 'netdata', 'netdata');
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Service port'));
|
||||
o.datatype = 'port';
|
||||
o.placeholder = '19999';
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
if (value < 1 || value > 65535) {
|
||||
return _('Port must be between 1 and 65535');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.option(form.Flag, 'enable_ssl', _('Enable SSL'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.DummyValue, 'cert_file', _('Cert file'));
|
||||
o.default = '/etc/ssl/ezopwrt.crt';
|
||||
o.depends('enable_ssl', '1');
|
||||
o.cfgvalue = function(section_id) {
|
||||
return uci.get('netdata', section_id, 'cert_file') || '/etc/ssl/ezopwrt.crt';
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, 'key_file', _('Cert Key file'));
|
||||
o.default = '/etc/ssl/ezopwrt.key';
|
||||
o.depends('enable_ssl', '1');
|
||||
o.cfgvalue = function(section_id) {
|
||||
return uci.get('netdata', section_id, 'key_file') || '/etc/ssl/ezopwrt.key';
|
||||
};
|
||||
|
||||
// o = s.option(form.DummyValue, 'feedback_info', _('feedback info'));
|
||||
// o.href = 'https://github.com/sirpdboy';
|
||||
// o.cfgvalue = function() {
|
||||
// return 'https://github.com/sirpdboy';
|
||||
// };
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@ -1,162 +1,36 @@
|
||||
// Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netdata
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require form';
|
||||
|
||||
const isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
return view.extend({
|
||||
// handleSaveApply: null,
|
||||
// handleSave: null,
|
||||
// handleReset: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.stat('/usr/sbin/nginx'), {}),
|
||||
uci.load('netdata')
|
||||
]);
|
||||
return L.resolveDefault(fs.read('/etc/netdata/netdata.conf'), '');
|
||||
},
|
||||
|
||||
render(res) {
|
||||
const has_nginx = res[0].path;
|
||||
handleSave(ev) {
|
||||
let value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n';
|
||||
|
||||
let m, s, o;
|
||||
return fs.write('/etc/netdata/netdata.conf', value).then((rc) => {
|
||||
document.querySelector('textarea').value = value;
|
||||
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
|
||||
|
||||
m = new form.Map('netdata');
|
||||
return fs.exec('/etc/init.d/netdata', [ 'restart' ]);
|
||||
}).catch((e) => {
|
||||
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)));
|
||||
});
|
||||
},
|
||||
|
||||
s = m.section(form.TypedSection, 'netdata');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Service port'));
|
||||
o.datatype = 'port';
|
||||
o.placeholder = '19999';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'enable_ssl', _('Enable SSL'));
|
||||
o.rmempty = true;
|
||||
o.depends('nginx_support', '0');
|
||||
|
||||
o = s.option(form.Value, 'cert_file', _('Cert file'));
|
||||
o.placeholder = '/etc/netdata/cert.crt';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('enable_ssl', '1');
|
||||
|
||||
o = s.option(form.Value, 'key_file', _('Cert Key file'));
|
||||
o.placeholder = '/etc/netdata/cert.key';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('enable_ssl', '1');
|
||||
|
||||
o = s.option(form.Flag, 'nginx_support', _('Nginx Support'));
|
||||
o.rmempty = true;
|
||||
if (! has_nginx) {
|
||||
o.description = _('To enable this feature you need install <b>luci-nginx</b> and <b>luci-ssl-nginx</b><br/> first');
|
||||
o.readonly = true;
|
||||
}
|
||||
|
||||
o.write = function(section, value) {
|
||||
uci.set('netdata', section, 'nginx_support', has_nginx ? value : null);
|
||||
};
|
||||
|
||||
o = s.option(form.Flag, 'auth', _('Enable Auth'));
|
||||
o.rmempty = true;
|
||||
o.depends('nginx_support', '1');
|
||||
|
||||
o.write = function(section, value) {
|
||||
uci.set('netdata', section, 'auth', has_nginx ? value : null);
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'user_passwd', _('Login Username and Password hash'));
|
||||
o.placeholder = 'admin:$apr1$t7qQjoqb$YBHtAb7VGSkjIdObMG.Oy0';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('auth', '1');
|
||||
|
||||
o = s.option(form.Button, '_webui', _('Open Web UI'));
|
||||
o.inputtitle = _('Open');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = L.bind(function(ev, section_id) {
|
||||
const port=uci.get('netdata', section_id, 'port') || '19999';
|
||||
const ssl=uci.get('netdata', section_id, 'enable_ssl') || '0';
|
||||
const nginx=uci.get('netdata', section_id, 'nginx_support') || '0';
|
||||
|
||||
window.open((nginx === '1' ? window.location.protocol : ssl === '1' ? 'https:' : 'http:') + '//' + window.location.hostname + (nginx === '1' ? '/netdata/' : ':' + port));
|
||||
}, o)
|
||||
|
||||
o = s.option(form.Button, '_start', _('Start') + ' ' + _('Netdata'));
|
||||
o.inputtitle = _('Start');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = function() {
|
||||
return fs.exec('/etc/init.d/netdata', ['start'])
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') });
|
||||
};
|
||||
|
||||
o = s.option(form.Button, '_stop', _('Stop') + ' ' + _('Netdata'));
|
||||
o.inputtitle = _('Stop');
|
||||
o.inputstyle = 'reset';
|
||||
o.onclick = function() {
|
||||
return fs.exec('/etc/init.d/netdata', ['stop'])
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') });
|
||||
};
|
||||
|
||||
s = m.section(form.TypedSection, '_utilities');
|
||||
s.render = L.bind(function(view, section_id) {
|
||||
return E('div',{ 'class': 'cbi-section' }, [
|
||||
E('h3', {}, [ _('Password hash generator') ]),
|
||||
E('div', { 'class': 'generator' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
let npwn = this.map.findElement('id', '_passwd_type');
|
||||
|
||||
if (npwn.type == 'password') {
|
||||
npwn.setAttribute('type', 'text');
|
||||
} else if (npwn.type == 'text') {
|
||||
npwn.setAttribute('type', 'password');
|
||||
}
|
||||
})
|
||||
}, '*'),
|
||||
E('input', {
|
||||
'id': '_passwd_type',
|
||||
'style': 'margin:5px 0',
|
||||
'class': 'cbi-input-password',
|
||||
'type': 'password',
|
||||
'value': 'type new passwd here'
|
||||
})
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
let npw = this.map.findElement('id', '_passwd_type').value,
|
||||
button = document.querySelector('.generator > .cbi-button'),
|
||||
out = this.map.findElement('id', '_passwd_hash_out');
|
||||
|
||||
button.setAttribute('disabled', 'true');
|
||||
|
||||
return fs.exec('/usr/bin/openssl', [ 'passwd', '-apr1', npw ]).then((res) => {
|
||||
out.value = res.stdout || '';
|
||||
}).catch(e => {
|
||||
ui.addNotification(null, E('p', e.message), 'error');
|
||||
}).finally(() => {
|
||||
button.removeAttribute('disabled');
|
||||
});
|
||||
})
|
||||
}, [ _('GenNew') ]),
|
||||
E('input', {
|
||||
'id': '_passwd_hash_out',
|
||||
'style': 'margin:5px 0; width:320px',
|
||||
'type': 'text',
|
||||
'readonly': ''
|
||||
})
|
||||
])
|
||||
]);
|
||||
}, o, this);
|
||||
|
||||
return m.render();
|
||||
render(conf) {
|
||||
return E([
|
||||
E('h4', _('Edit Netdata main config: <code>/etc/netdata/netdata.conf</code>')),
|
||||
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 25, 'disabled': isReadonlyView }, [ conf != null ? conf : '' ]))
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require uci';
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
load() {
|
||||
return uci.load('netdata');
|
||||
},
|
||||
|
||||
render() {
|
||||
var port = uci.get_first('netdata', 'netdata', 'port') || '19999',
|
||||
ssl = uci.get_first('netdata', 'netdata', 'enable_ssl') || '0',
|
||||
nginx = uci.get_first('netdata', 'netdata', 'nginx_support') || '0';
|
||||
if (port === '0')
|
||||
return E('div', { class: 'alert-message warning' },
|
||||
_('Port 0 is not supported.<br />Change to a other port and try again.'));
|
||||
return E('iframe', {
|
||||
src: (nginx === '1' ? window.location.protocol : ssl === '1' ? 'https:' : 'http:') + '//' + window.location.hostname + (nginx === '1' ? '/netdata/' : ':' + port),
|
||||
style: 'width: 100%; min-height: 100vh; border: none; border-radius: 3px;'
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2021-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netdata
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require poll';
|
||||
|
||||
return view.extend({
|
||||
|
||||
load: function() {
|
||||
return uci.load('netdata');
|
||||
},
|
||||
|
||||
checkRunning: function() {
|
||||
return fs.exec('/bin/pidof', ['netdata']).then(function(pidRes) {
|
||||
if (pidRes.code === 0) return { isRunning: true };
|
||||
return fs.exec('/bin/ash', ['-c', 'ps | grep -q "[n]etdata"']).then(function(grepRes) {
|
||||
return { isRunning: grepRes.code === 0 };
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
return this.checkRunning().then(function(checkResult) {
|
||||
var isRunning = checkResult.isRunning;
|
||||
let webport = uci.get_first('netdata', 'netdata', 'port') || '19999';
|
||||
let uci_ssl = uci.get_first('netdata', 'netdata', 'enable_ssl') || '0';
|
||||
let nginx = uci.get_first('netdata', 'netdata', 'nginx_support') || '0';
|
||||
let protocol = nginx === '1' ? window.location.protocol : uci_ssl === '1' ? 'https:' : 'http:';
|
||||
let buttonUrl = String.format('%s//%s:%s/#menu_system', protocol, window.location.hostname, webport);
|
||||
|
||||
var container = E('div');
|
||||
if (!isRunning) {
|
||||
var message = _('Netdata Service Not Running');
|
||||
container.appendChild(E('div', {
|
||||
style: 'text-align: center; padding: 2em;'
|
||||
}, [
|
||||
E('img', {
|
||||
src: 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCI+PHBhdGggZmlsbD0iI2RmMDAwMCIgZD0iTTk0Mi40MjEgMjM0LjYyNGw4MC44MTEtODAuODExLTE1My4wNDUtMTUzLjA0NS04MC44MTEgODAuODExYy03OS45NTctNTEuNjI3LTE3NS4xNDctODEuNTc5LTI3Ny4zNzYtODEuNTc5LTI4Mi43NTIgMC01MTIgMjI5LjI0OC01MTIgNTEyIDAgMTAyLjIyOSAyOS45NTIgMTk3LjQxOSA4MS41NzkgMjc3LjM3NmwtODAuODExIDgwLjgxMSAxNTMuMDQ1IDE1My4wNDUgODAuODExLTgwLjgxMWM3OS45NTcgNTEuNjI3IDE3NS4xNDcgODEuNTc5IDI3Ny4zNzYgODEuNTc5IDI4Mi43NTIgMCA1MTItMjI5LjI0OCA1MTItNTEyIDAtMTAyLjIyOS0yOS45NTItMTk3LjQxOS04MS41NzktMjc3LjM3NnpNMTk0Ljk0NCA1MTJjMC0xNzUuMTA0IDE0MS45NTItMzE3LjA1NiAzMTcuMDU2LTMxNy4wNTYgNDggMCA5My40ODMgMTAuNjY3IDEzNC4yMjkgMjkuNzgxbC00MjEuNTQ3IDQyMS41NDdjLTE5LjA3Mi00MC43ODktMjkuNzM5LTg2LjI3Mi0yOS43MzktMTM0LjI3MnpNNTEyIDgyOS4wNTZjLTQ4IDAtOTMuNDgzLTEwLjY2Ny0xMzQuMjI5LTI5Ljc4MWw0MjEuNTQ3LTQyMS41NDdjMTkuMDcyIDQwLjc4OSAyOS43ODEgODYuMjcyIDI5Ljc4MSAxMzQuMjI5LTAuMDQzIDE3NS4xNDctMTQxLjk5NSAzMTcuMDk5LTMxNy4wOTkgMzE3LjA5OXoiLz48L3N2Zz4=',
|
||||
style: 'width: 100px; height: 100px; margin-bottom: 1em;'
|
||||
}),
|
||||
E('h2', {}, message)
|
||||
]));
|
||||
} else {
|
||||
var iframe = E('iframe', {
|
||||
src: buttonUrl,
|
||||
style: 'width: 100%; min-height: 100vh; border: none;'
|
||||
});
|
||||
container.appendChild(iframe);
|
||||
}
|
||||
|
||||
poll.add(function() {
|
||||
return self.checkRunning().then(function(checkResult) {
|
||||
var newStatus = checkResult.isRunning;
|
||||
if (newStatus !== isRunning) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}, 5);
|
||||
|
||||
poll.start();
|
||||
|
||||
return container;
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -1,93 +0,0 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:30
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:22
|
||||
msgid "Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:21
|
||||
msgid "Contents have been saved."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:31
|
||||
msgid "Edit Netdata main config: <code>/etc/netdata/netdata.conf</code>"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:30
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:49
|
||||
msgid "Enable Auth"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:38
|
||||
msgid "Enable SSL"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:63
|
||||
msgid "Enable logger"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:134
|
||||
msgid "GenNew"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/rpcd/acl.d/luci-app-netdata.json:3
|
||||
msgid "Grant UCI access for netdata"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:57
|
||||
msgid "Login Username and Password hash"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:75
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:83
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:3
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:14
|
||||
msgid "Netdata"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:67
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:66
|
||||
msgid "Open Web UI"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:94
|
||||
msgid "Password hash generator"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/main.js:19
|
||||
msgid "Port 0 is not supported.<br />Change to a other port and try again."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:33
|
||||
msgid "Service port"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:75
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:76
|
||||
msgid "Start"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:83
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:84
|
||||
msgid "Stop"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:41
|
||||
msgid ""
|
||||
"To enable this feature you need install <b>luci-nginx</b> and <b>luci-ssl-"
|
||||
"nginx</b><br/> first"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:25
|
||||
msgid "Unable to save contents: %s"
|
||||
msgstr ""
|
||||
@ -1,104 +1,55 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"PO-Revision-Date: 2023-04-28 11:30+0000\n"
|
||||
"Last-Translator: muink <hukk1996@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
|
||||
"openwrt/luciapplicationsnetdata/zh_Hans/>\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.12.1-dev\n"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:30
|
||||
msgid "Advanced"
|
||||
msgstr "高级"
|
||||
msgid "Netdata"
|
||||
msgstr "实时监控"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:22
|
||||
msgid "Config"
|
||||
msgstr "配置"
|
||||
msgid "Basic Setting"
|
||||
msgstr "基本设置"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:21
|
||||
msgid "Contents have been saved."
|
||||
msgstr ""
|
||||
msgid "Config Setting"
|
||||
msgstr "配置文件"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:31
|
||||
msgid "Edit Netdata main config: <code>/etc/netdata/netdata.conf</code>"
|
||||
msgstr "编辑 Netdata 主配置: <code>/etc/netdata/netdata.conf</code>"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:30
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:49
|
||||
msgid "Enable Auth"
|
||||
msgstr "启用验证"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:38
|
||||
|
||||
msgid "Enable SSL"
|
||||
msgstr "启用 SSL"
|
||||
msgstr "SSL启用https访问"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:63
|
||||
msgid "Enable logger"
|
||||
msgstr "启用日志记录器"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:134
|
||||
msgid "GenNew"
|
||||
msgstr "生成新的"
|
||||
msgid "An open-source real-time performance monitoring and visualization tool"
|
||||
msgstr "一款开源的实时性能监控与可视化工具"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/rpcd/acl.d/luci-app-netdata.json:3
|
||||
msgid "Grant UCI access for netdata"
|
||||
msgstr "授予访问 LuCI 应用 netdata 的权限"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:57
|
||||
msgid "Login Username and Password hash"
|
||||
msgstr "登入用户名和密码哈希"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:75
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:83
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:3
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:14
|
||||
msgid "Netdata"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:67
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:66
|
||||
msgid "Open Web UI"
|
||||
msgstr "打开 Web UI"
|
||||
msgstr "打开Web网页"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:94
|
||||
msgid "Password hash generator"
|
||||
msgstr "密码哈希生成器"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/main.js:19
|
||||
msgid "Port 0 is not supported.<br />Change to a other port and try again."
|
||||
msgstr "不支持端口 0。<br />改为其他端口然后再试一次。"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:33
|
||||
msgid "Service port"
|
||||
msgstr "服务端口"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:75
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:76
|
||||
msgid "Start"
|
||||
msgstr "启动"
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:83
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:84
|
||||
msgid "Stop"
|
||||
msgstr "停止"
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Netdata Service Not Running"
|
||||
msgstr "Netdata服务未启用"
|
||||
|
||||
msgid "Cert file"
|
||||
msgstr "Cert文件"
|
||||
|
||||
msgid "Cert Key file"
|
||||
msgstr "Cert密钥文件"
|
||||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:41
|
||||
msgid ""
|
||||
"To enable this feature you need install <b>luci-nginx</b> and <b>luci-ssl-"
|
||||
"nginx</b><br/> first"
|
||||
msgstr "要启用此功能必须先安装 <b>luci-nginx</b> 和 <b>luci-ssl-nginx</b><br/>"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:25
|
||||
msgid "Unable to save contents: %s"
|
||||
msgstr ""
|
||||
|
||||
@ -1,104 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"PO-Revision-Date: 2023-04-28 11:30+0000\n"
|
||||
"Last-Translator: muink <hukk1996@gmail.com>\n"
|
||||
"Language-Team: Chinese (Traditional) <https://hosted.weblate.org/projects/"
|
||||
"openwrt/luciapplicationsnetdata/zh_Hant/>\n"
|
||||
"Language: zh_Hant\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.12.1-dev\n"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:30
|
||||
msgid "Advanced"
|
||||
msgstr "進階"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:22
|
||||
msgid "Config"
|
||||
msgstr "組態"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:21
|
||||
msgid "Contents have been saved."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:31
|
||||
msgid "Edit Netdata main config: <code>/etc/netdata/netdata.conf</code>"
|
||||
msgstr "編輯 Netdata 主要配置: <code>/etc/netdata/netdata.conf</code>"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:30
|
||||
msgid "Enable"
|
||||
msgstr "啟用"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:49
|
||||
msgid "Enable Auth"
|
||||
msgstr "啟用認證"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:38
|
||||
msgid "Enable SSL"
|
||||
msgstr "啟用 SSL"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:63
|
||||
msgid "Enable logger"
|
||||
msgstr "啟用日誌記錄器"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:134
|
||||
msgid "GenNew"
|
||||
msgstr "生成新的"
|
||||
|
||||
#: applications/luci-app-netdata/root/usr/share/rpcd/acl.d/luci-app-netdata.json:3
|
||||
msgid "Grant UCI access for netdata"
|
||||
msgstr "授予訪問 LuCI 應用 netdata 的權限"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:57
|
||||
msgid "Login Username and Password hash"
|
||||
msgstr "登入用戶名和密碼雜湊"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:75
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:83
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:3
|
||||
#: applications/luci-app-netdata/root/usr/share/luci/menu.d/luci-app-netdata.json:14
|
||||
msgid "Netdata"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:67
|
||||
msgid "Open"
|
||||
msgstr "開啟"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:66
|
||||
msgid "Open Web UI"
|
||||
msgstr "開啟 Web UI"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:94
|
||||
msgid "Password hash generator"
|
||||
msgstr "密碼雜湊產生器"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/main.js:19
|
||||
msgid "Port 0 is not supported.<br />Change to a other port and try again."
|
||||
msgstr "不支援埠號為 0。<br />變更為其他埠後重試。"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:33
|
||||
msgid "Service port"
|
||||
msgstr "伺服通訊埠"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:75
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:76
|
||||
msgid "Start"
|
||||
msgstr "啟動"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:83
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:84
|
||||
msgid "Stop"
|
||||
msgstr "停止"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/config.js:41
|
||||
msgid ""
|
||||
"To enable this feature you need install <b>luci-nginx</b> and <b>luci-ssl-"
|
||||
"nginx</b><br/> first"
|
||||
msgstr "要啟用此功能必須先安裝 <b>luci-nginx</b> 和 <b>luci-ssl-nginx</b><br/>"
|
||||
|
||||
#: applications/luci-app-netdata/htdocs/luci-static/resources/view/netdata/advanced.js:25
|
||||
msgid "Unable to save contents: %s"
|
||||
msgstr ""
|
||||
28
luci-app-netdata/root/etc/uci-defaults/luci-app-netdata
Normal file
28
luci-app-netdata/root/etc/uci-defaults/luci-app-netdata
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
[ ! -f "/usr/share/ucitrack/luci-app-netdata.json" ] && {
|
||||
cat > /usr/share/ucitrack/luci-app-netdata.json << EEOF
|
||||
{
|
||||
"config": "netdata",
|
||||
"init": "netdata"
|
||||
}
|
||||
EEOF
|
||||
}
|
||||
# [ -f /usr/share/netdata/webcn/netdata.conf ] && mv -f /usr/share/netdata/webcn/netdata.conf /etc/netdata/netdata.conf
|
||||
[ -f /usr/share/netdata/webcn/netdata.config ] && mv -f /usr/share/netdata/webcn/netdata.config /etc/config/netdata
|
||||
[ -f /usr/share/netdata/webcn/netdata.init ] && mv -f /usr/share/netdata/webcn/netdata.init /etc/init.d/netdata
|
||||
|
||||
[ -f "/etc/config/ucitrack" ] && {
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@netdata[-1]
|
||||
add ucitrack netdata
|
||||
set ucitrack.@netdata[-1].init=netdata
|
||||
commit ucitrack
|
||||
EOF
|
||||
}
|
||||
|
||||
rm -f /tmp/luci-indexcache* 2>/dev/null
|
||||
rm -f /tmp/luci-modulecache/* 2>/dev/null # OpenWrt 21.02
|
||||
rm -f /tmp/luci-indexcache
|
||||
|
||||
rm -rf /tmp/luci-*
|
||||
exit 0
|
||||
@ -12,26 +12,26 @@
|
||||
},
|
||||
"admin/status/netdata/netdata": {
|
||||
"title": "Netdata",
|
||||
"order": 1,
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "netdata/main"
|
||||
"path": "netdata/netdata"
|
||||
}
|
||||
},
|
||||
"admin/status/netdata/basic": {
|
||||
"title": "Basic Setting",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "netdata/basic"
|
||||
}
|
||||
},
|
||||
"admin/status/netdata/config": {
|
||||
"title": "Config",
|
||||
"order": 2,
|
||||
"title": "Config Setting",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "netdata/config"
|
||||
}
|
||||
},
|
||||
"admin/status/netdata/advanced": {
|
||||
"title": "Advanced",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "netdata/advanced"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
luci-app-netdata/root/usr/share/netdata/webcn/netdata.conf
Normal file
33
luci-app-netdata/root/usr/share/netdata/webcn/netdata.conf
Normal file
@ -0,0 +1,33 @@
|
||||
# Full configuration can be retrieved from the running
|
||||
# server at http://localhost:19999/netdata.conf
|
||||
#
|
||||
# Example:
|
||||
# curl -o /etc/netdata/netdata.conf http://localhost:19999/netdata.conf
|
||||
#
|
||||
|
||||
[global]
|
||||
update every = 2
|
||||
memory deduplication (ksm) = no
|
||||
debug log = none
|
||||
error log = none
|
||||
access log = none
|
||||
run as user = root
|
||||
|
||||
[web]
|
||||
default port = 19999
|
||||
allow connections from = *
|
||||
allow dashboard from = *
|
||||
|
||||
[plugins]
|
||||
cgroups = no
|
||||
apps = no
|
||||
charts.d = no
|
||||
fping = no
|
||||
node.d = no
|
||||
python.d = no
|
||||
|
||||
[health]
|
||||
enabled = no
|
||||
|
||||
[plugin:proc:ipc]
|
||||
shared memory totals = no
|
||||
@ -0,0 +1,3 @@
|
||||
config netdata 'netdata'
|
||||
option enabled '0'
|
||||
option port '19999'
|
||||
105
luci-app-netdata/root/usr/share/netdata/webcn/netdata.init
Normal file
105
luci-app-netdata/root/usr/share/netdata/webcn/netdata.init
Normal file
@ -0,0 +1,105 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
|
||||
APPBINARY=/usr/sbin/netdata
|
||||
CONFIGFILE=/etc/netdata/netdata.conf
|
||||
|
||||
config_load 'netdata'
|
||||
|
||||
get_config() {
|
||||
config_get_bool enabled $1 enabled 0
|
||||
config_get port $1 port 19999
|
||||
config_get_bool ssl $1 enable_ssl 0
|
||||
config_get cert_file $1 cert_file '/etc/ssl/ezopwrt.crt'
|
||||
config_get key_file $1 key_file '/etc/ssl/ezopwrt.key'
|
||||
config_get_bool ennginx $1 nginx_support 0
|
||||
config_get_bool auth $1 auth 0
|
||||
config_get userpw $1 user_passwd 'admin:$apr1$t7qQjoqb$YBHtAb7VGSkjIdObMG.Oy0'
|
||||
[ "$ennginx" == "1" ] && ssl=0
|
||||
}
|
||||
|
||||
#read_option <option>
|
||||
read_option() {
|
||||
[ -n "$1" ] || return 1
|
||||
sed -En "/^\[web\]/,/^(\[|$)/{s|^\s*$1 = *(.*)$|\1|p}" "$CONFIGFILE"
|
||||
}
|
||||
|
||||
#white_option <option> [value]
|
||||
white_option() {
|
||||
[ -n "$1" ] || return 1
|
||||
if [ -n "$(sed -En "/^\[web\]/,/^(\[|$)/{s|^\s*($1) =.*$|\1|p}" "$CONFIGFILE")" ]; then
|
||||
sed -Ei "/^\[web\]/,/^(\[|$)/{s|^(\s*)$1 =.*|\1$1 = $2|}" "$CONFIGFILE"
|
||||
else
|
||||
sed -Ei "/^\[web\]/a\\\t$1 = $2" "$CONFIGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
white_conf() {
|
||||
if [ "x$ssl" == "x1" ] ;then
|
||||
rm -f $certskey $certscrt
|
||||
hostname=$(uci -q get system.@system[0].hostname | awk '{print tolower($0)}')
|
||||
uci -q set network.lan.hostname=${hostname}
|
||||
uci -q set dhcp.@dnsmasq[0].domain="${hostname}.lan"
|
||||
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout $key_file -out $cert_file -subj "/C=CN/CN=$hostname.lan"
|
||||
uci commit dhcp
|
||||
uci commit network
|
||||
else
|
||||
local cert_file='' key_file=''
|
||||
fi
|
||||
[ "$port" == "$(read_option 'default port')" ] || white_option 'default port' "$port"
|
||||
[ "$cert_file" == "$(read_option 'ssl certificate')" ] || white_option 'ssl certificate' "$cert_file"
|
||||
[ "$key_file" == "$(read_option 'ssl key')" ] || white_option 'ssl key' "$key_file"
|
||||
}
|
||||
|
||||
write_nginx() {
|
||||
local location="/etc/nginx/conf.d/netdata.locations"
|
||||
local htpasswd="/etc/nginx/conf.d/netdata.htpasswd"
|
||||
|
||||
echo "$userpw" > "$htpasswd"
|
||||
[ "$(sed -En "s|^\s*proxy_pass\s+https?://[^:]+:(\d+).*|\1|p" "$location")" == "$port" ] || sed -Ei "/proxy_pass/{s|(\bproxy_pass\b)(\s+.+)?|\1 http://localhost:$port/;|}" "$location"
|
||||
if [ "$auth" == "1" ];then
|
||||
sed -Ei "s|#(\bauth_basic_user_file\b)|\1|" "$location"
|
||||
else
|
||||
sed -Ei "s|^(\s*)(\bauth_basic_user_file\b)|\1#\2|" "$location"
|
||||
fi
|
||||
/etc/init.d/nginx reload
|
||||
}
|
||||
|
||||
netdata_instance() {
|
||||
mkdir -m 0755 -p /var/cache/netdata
|
||||
chown nobody /var/cache/netdata
|
||||
mkdir -m 0755 -p /var/lib/netdata
|
||||
chown nobody /var/lib/netdata
|
||||
mkdir -m 0755 -p /var/lib/netdata/cloud.d
|
||||
chown nobody /var/lib/netdata/cloud.d
|
||||
mkdir -m 0755 -p /var/log/netdata
|
||||
chown nobody /var/log/netdata
|
||||
procd_open_instance
|
||||
procd_set_param command $APPBINARY -D -c $CONFIGFILE
|
||||
procd_set_param file $CONFIGFILE
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_foreach get_config 'netdata'
|
||||
[ "${enabled:=0}" == "0" ] && stop_service && return 1
|
||||
white_conf
|
||||
[ "$ennginx" == "1" ] && write_nginx
|
||||
netdata_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
pgrep -f $APPBINARY | xargs kill -9 >/dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger 'netdata'
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@ -5,10 +5,15 @@
|
||||
"file": {
|
||||
"/etc/netdata/netdata.conf": [ "read" ],
|
||||
"/etc/init.d/netdata": [ "exec" ],
|
||||
"/usr/bin/openssl passwd -apr1 *": [ "exec" ]
|
||||
"/usr/bin/openssl passwd -apr1 *": [ "exec" ],
|
||||
"/bin/pidof": [ "exec" ],
|
||||
"/bin/ps": [ "exec" ],
|
||||
"/bin/ash": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "read" ]
|
||||
"rc": [ "*" ],
|
||||
"file": [ "read" ],
|
||||
"luci.netdata": [ "*" ]
|
||||
},
|
||||
"uci": [ "netdata" ]
|
||||
},
|
||||
@ -17,7 +22,8 @@
|
||||
"/etc/netdata/netdata.conf": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "write" ]
|
||||
"file": [ "write" ],
|
||||
"luci.netdata": [ "*" ]
|
||||
},
|
||||
"uci": [ "netdata" ]
|
||||
}
|
||||
|
||||
53
luci-app-netdata/root/usr/share/rpcd/ucode/luci.netdata
Normal file
53
luci-app-netdata/root/usr/share/rpcd/ucode/luci.netdata
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2021-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-netdata
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { access, popen } from 'fs';
|
||||
|
||||
function get_netdata_version() {
|
||||
if (!access('/usr/sbin/netdata'))
|
||||
return null;
|
||||
|
||||
const fd = popen("a=`/usr/sbin/netdata -v`&&echo $a |awk -F v '{printf $2}'");
|
||||
if (fd) {
|
||||
let output = trim(fd.read('all'));
|
||||
fd.close();
|
||||
// 方法1: 简单替换(推荐)
|
||||
let version_with_v = replace(output, "netdata ", "");
|
||||
// 方法2: 去掉 v 前缀
|
||||
let version_clean = replace(version_with_v, "v", "");
|
||||
return version_with_v; // 返回 "v2.3.2"
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
get_version: {
|
||||
call: function() {
|
||||
let version = get_netdata_version();
|
||||
if (!version)
|
||||
return { version: null, error: '无法获取 netdata 版本' };
|
||||
|
||||
return { version: version };
|
||||
}
|
||||
},
|
||||
|
||||
get_version_clean: {
|
||||
call: function() {
|
||||
let version = get_netdata_version();
|
||||
if (!version)
|
||||
return { version: null, error: '无法获取 netdata 版本' };
|
||||
|
||||
// 去掉 "v" 前缀,返回纯数字版本
|
||||
let clean_version = replace(version, /^v/, "");
|
||||
return { version: clean_version };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.netdata': methods };
|
||||
@ -523,6 +523,11 @@ o.description = desc
|
||||
.. "</ul>"
|
||||
o:depends({dns_shunt = "chinadns-ng", tcp_proxy_mode = "proxy", chn_list = "direct"})
|
||||
|
||||
o = s:option(Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
o:depends({dns_shunt = "chinadns-ng"})
|
||||
|
||||
o = s:option(ListValue, "use_default_dns", translate("Default DNS"))
|
||||
o.default = "direct"
|
||||
o:value("remote", translate("Remote DNS"))
|
||||
|
||||
@ -523,8 +523,7 @@ if api.is_finded("smartdns") then
|
||||
end
|
||||
|
||||
o = s:taboption("DNS", Flag, "force_https_soa", translate("Force HTTPS SOA"), translate("Force queries with qtype 65 to respond with an SOA record."))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
o.default = "0"
|
||||
o:depends({dns_shunt = "chinadns-ng"})
|
||||
if api.is_finded("smartdns") then
|
||||
o:depends({dns_shunt = "smartdns"})
|
||||
|
||||
@ -370,7 +370,7 @@ o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
-- o:value("1.3")
|
||||
-- o:depends({ [_n("tls")] = true })
|
||||
|
||||
o = s:option(Value, _n("tls_serverName"), translate("Domain"))
|
||||
o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
|
||||
o:depends({ [_n("tls")] = true })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
|
||||
@ -427,7 +427,7 @@ o:depends({ [_n("protocol")] = "hysteria"})
|
||||
o:depends({ [_n("protocol")] = "tuic" })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
o = s:option(Value, _n("tls_serverName"), translate("Domain"))
|
||||
o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
|
||||
o:depends({ [_n("tls")] = true })
|
||||
o:depends({ [_n("protocol")] = "hysteria"})
|
||||
o:depends({ [_n("protocol")] = "tuic" })
|
||||
|
||||
@ -1427,7 +1427,7 @@ function gen_config(var)
|
||||
elseif remote_dns_query_strategy == "UseIPv6" then
|
||||
table.insert(fakedns, fakedns6)
|
||||
end
|
||||
if remote_dns_fake and inner_fakedns == "0" then
|
||||
if remote_dns_fake and inner_fakedns ~= "1" then
|
||||
table.insert(dns.servers, 1, _remote_fakedns)
|
||||
end
|
||||
end
|
||||
|
||||
@ -301,7 +301,7 @@ run_dns2socks() {
|
||||
}
|
||||
|
||||
run_chinadns_ng() {
|
||||
local _flag _listen_port _dns_local _dns_trust _no_ipv6_trust _use_direct_list _use_proxy_list _gfwlist _chnlist _default_mode _default_tag _no_logic_log _tcp_node _remote_fakedns
|
||||
local _flag _listen_port _dns_local _dns_trust _no_ipv6_trust _use_direct_list _use_proxy_list _gfwlist _chnlist _default_mode _default_tag _no_logic_log _tcp_node _remote_fakedns _filter_https
|
||||
local _extra_param=""
|
||||
eval_set_val $@
|
||||
|
||||
@ -313,7 +313,7 @@ run_chinadns_ng() {
|
||||
_extra_param="${_extra_param} -USE_DIRECT_LIST ${_use_direct_list} -USE_PROXY_LIST ${_use_proxy_list} -USE_BLOCK_LIST ${_use_block_list}"
|
||||
_extra_param="${_extra_param} -GFWLIST ${_gfwlist} -CHNLIST ${_chnlist} -NO_IPV6_TRUST ${_no_ipv6_trust} -DEFAULT_MODE ${_default_mode}"
|
||||
_extra_param="${_extra_param} -DEFAULT_TAG ${_default_tag} -NFTFLAG ${nftflag} -NO_LOGIC_LOG ${_no_logic_log} -REMOTE_FAKEDNS ${_remote_fakedns}"
|
||||
_extra_param="${_extra_param} -LOG_FILE ${_LOG_FILE}"
|
||||
_extra_param="${_extra_param} -FILTER_HTTPS ${_filter_https} -LOG_FILE ${_LOG_FILE}"
|
||||
|
||||
lua $APP_PATH/helper_chinadns_add.lua ${_extra_param} > ${_CONF_FILE}
|
||||
ln_run "$(first_type chinadns-ng)" chinadns-ng "${_LOG_FILE}" -C ${_CONF_FILE}
|
||||
@ -1478,7 +1478,8 @@ start_dns() {
|
||||
_default_tag=$(config_t_get global chinadns_ng_default_tag smart) \
|
||||
_no_logic_log=0 \
|
||||
_tcp_node=${TCP_NODE} \
|
||||
_remote_fakedns=${fakedns:-0}
|
||||
_remote_fakedns=${fakedns:-0} \
|
||||
_filter_https=$(config_t_get global force_https_soa 0)
|
||||
|
||||
USE_DEFAULT_DNS="chinadns_ng"
|
||||
}
|
||||
@ -1694,7 +1695,8 @@ acl_app() {
|
||||
_default_tag=${chinadns_ng_default_tag:-smart} \
|
||||
_no_logic_log=1 \
|
||||
_tcp_node=${tcp_node} \
|
||||
_remote_fakedns=0
|
||||
_remote_fakedns=${remote_fakedns:-0} \
|
||||
_filter_https=${force_https_soa:-0}
|
||||
|
||||
use_default_dns="chinadns_ng"
|
||||
}
|
||||
@ -1834,7 +1836,7 @@ acl_app() {
|
||||
}
|
||||
unset enabled sid remarks sources interface tcp_no_redir_ports udp_no_redir_ports use_global_config tcp_node udp_node use_direct_list use_proxy_list use_block_list use_gfw_list chn_list tcp_proxy_mode udp_proxy_mode filter_proxy_ipv6 dns_mode remote_dns v2ray_dns_mode remote_dns_doh remote_dns_client_ip
|
||||
unset _ip _mac _iprange _ipset _ip_or_mac source_list tcp_port udp_port config_file _extra_param
|
||||
unset _china_ng_listen _chinadns_local_dns _direct_dns_mode chinadns_ng_default_tag dnsmasq_filter_proxy_ipv6
|
||||
unset _china_ng_listen _chinadns_local_dns _direct_dns_mode chinadns_ng_default_tag dnsmasq_filter_proxy_ipv6 remote_fakedns force_https_soa
|
||||
done
|
||||
unset socks_port redir_port dns_port dnsmasq_port chinadns_port
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"]
|
||||
local TCP_NODE = var["-TCP_NODE"]
|
||||
local NFTFLAG = var["-NFTFLAG"]
|
||||
local REMOTE_FAKEDNS = var["-REMOTE_FAKEDNS"]
|
||||
local FILTER_HTTPS = var["-FILTER_HTTPS"]
|
||||
local LOG_FILE = var["-LOG_FILE"]
|
||||
|
||||
local uci = api.uci
|
||||
@ -113,15 +114,13 @@ local setflag = (NFTFLAG == "1") and "inet@passwall@" or ""
|
||||
|
||||
local only_global = (DEFAULT_MODE == "proxy" and CHNLIST == "0" and GFWLIST == "0") and 1
|
||||
|
||||
local force_https_soa = uci:get(appname, "@global[0]", "force_https_soa") or 1
|
||||
|
||||
config_lines = {
|
||||
LOG_FILE ~= "/dev/null" and "verbose" or "",
|
||||
"bind-addr ::",
|
||||
"bind-port " .. LISTEN_PORT,
|
||||
"china-dns " .. DNS_LOCAL,
|
||||
"trust-dns " .. DNS_TRUST,
|
||||
tonumber(force_https_soa) == 1 and "filter-qtype 65" or ""
|
||||
tonumber(FILTER_HTTPS) == 1 and "filter-qtype 65" or ""
|
||||
}
|
||||
|
||||
for i = 1, 6 do
|
||||
|
||||
@ -188,7 +188,7 @@ if not REMOTE_GROUP or REMOTE_GROUP == "nil" then
|
||||
sys.call('sed -i "/passwall/d" /etc/smartdns/custom.conf >/dev/null 2>&1')
|
||||
end
|
||||
|
||||
local force_https_soa = uci:get(appname, "@global[0]", "force_https_soa") or 1
|
||||
local force_https_soa = uci:get(appname, "@global[0]", "force_https_soa") or 0
|
||||
local proxy_server_name = "passwall-proxy-server"
|
||||
config_lines = {
|
||||
tonumber(LISTEN_PORT) ~= 0 and "bind [::]:" .. LISTEN_PORT .. "@lo" or "",
|
||||
|
||||
@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=190
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
2
luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js
vendored
Normal file
2
luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,49 @@
|
||||
-- Copyright (C) 2017 yushi studio <ywb94@qq.com>
|
||||
-- Licensed to the public under the GNU General Public License v3.
|
||||
module("luci.controller.shadowsocksr", package.seeall)
|
||||
require "nixio"
|
||||
require "nixio.fs"
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
|
||||
local function sh_uci_commit(config)
|
||||
luci.sys.call(string.format("uci -q commit %s", config))
|
||||
end
|
||||
|
||||
local function is_old_uci()
|
||||
return luci.sys.call("grep -E 'require[ \t]*\"uci\"' /usr/lib/lua/luci/model/uci.lua >/dev/null 2>&1") == 0
|
||||
end
|
||||
|
||||
local function uci_save(cursor, config, commit, apply)
|
||||
if is_old_uci() then
|
||||
cursor:save(config)
|
||||
if commit then
|
||||
cursor:commit(config)
|
||||
if apply then
|
||||
luci.sys.call("/etc/init.d/" .. config .. " reload > /dev/null 2>&1 &")
|
||||
end
|
||||
end
|
||||
else
|
||||
commit = true
|
||||
if commit then
|
||||
if apply then
|
||||
cursor:commit(config)
|
||||
else
|
||||
sh_uci_commit(config)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function url(...)
|
||||
local url = string.format("admin/services/%s", "shadowsocksr")
|
||||
local args = { ... }
|
||||
for i, v in ipairs(args) do
|
||||
if v and v ~= "" then
|
||||
url = url .. "/" .. v
|
||||
end
|
||||
end
|
||||
return require "luci.dispatcher".build_url(url)
|
||||
end
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/shadowsocksr") then
|
||||
@ -28,10 +71,14 @@ function index()
|
||||
entry({"admin", "services", "shadowsocksr", "reset"}, call("act_reset"))
|
||||
entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart"))
|
||||
entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete"))
|
||||
--[[ API ]]
|
||||
entry({"admin", "services", "shadowsocksr", "add_node"}, call("act_add_node")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "remove_node"}, call("act_remove"))
|
||||
entry({"admin", "services", "shadowsocksr", "save_node_order"}, call("act_save_order")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "get_now_use_node"}, call("act_get_now_use_node")).leaf = true
|
||||
entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况
|
||||
--[[Backup]]
|
||||
--[[Backup]]
|
||||
entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
|
||||
|
||||
end
|
||||
|
||||
function check_site(host, port)
|
||||
@ -103,78 +150,132 @@ function act_status()
|
||||
end
|
||||
|
||||
function act_ping()
|
||||
local e = {}
|
||||
local domain = luci.http.formvalue("domain")
|
||||
local port = tonumber(luci.http.formvalue("port") or 0)
|
||||
local transport = luci.http.formvalue("transport")
|
||||
local wsPath = luci.http.formvalue("wsPath") or ""
|
||||
local tls = luci.http.formvalue("tls")
|
||||
e.index = luci.http.formvalue("index")
|
||||
local e = {}
|
||||
local domain = luci.http.formvalue("domain")
|
||||
local port = tonumber(luci.http.formvalue("port") or 0)
|
||||
local transport = (luci.http.formvalue("transport") or ""):lower()
|
||||
local wsPath = luci.http.formvalue("wsPath") or ""
|
||||
local tls = luci.http.formvalue("tls")
|
||||
local host = luci.http.formvalue("host")
|
||||
local type = (luci.http.formvalue("type") or ""):lower()
|
||||
local proto = (luci.http.formvalue("proto") or ""):lower()
|
||||
e.index = luci.http.formvalue("index")
|
||||
|
||||
local use_nft = luci.sys.call("command -v nft >/dev/null") == 0
|
||||
local iret = false
|
||||
local is_ip = domain and domain:match("^%d+%.%d+%.%d+%.%d+$")
|
||||
|
||||
if use_nft then
|
||||
iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null") == 0
|
||||
else
|
||||
iret = luci.sys.call("ipset add ss_spec_wan_ac " .. domain .. " 2>/dev/null") == 0
|
||||
end
|
||||
-- 临时放行防火墙逻辑
|
||||
local use_nft = luci.sys.call("command -v nft >/dev/null") == 0
|
||||
local iret = false
|
||||
if domain then
|
||||
if use_nft then
|
||||
iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null") == 0
|
||||
else
|
||||
iret = luci.sys.call("ipset add ss_spec_wan_ac " .. domain .. " 2>/dev/null") == 0
|
||||
end
|
||||
end
|
||||
-- Hysteria2 节点检测
|
||||
if proto:find("hysteria2") or type:find("hysteria2") then
|
||||
local node_id = e.index
|
||||
-- 调用Shell测试脚本
|
||||
local cmd = string.format(
|
||||
"/usr/share/shadowsocksr/hy2_test.sh url_test_hy2 %s",
|
||||
node_id
|
||||
)
|
||||
local res = luci.sys.exec(cmd) or ""
|
||||
-- 解析结果
|
||||
local http_code, time_pre = string.match(res, "(%d+):([%d%.]+)")
|
||||
if http_code == "200" or http_code == "204" then
|
||||
e.socket = true
|
||||
e.ping = math.floor(tonumber(time_pre or 0) * 1000)
|
||||
else
|
||||
e.socket = false
|
||||
e.ping = 0
|
||||
end
|
||||
elseif transport == "ws" then
|
||||
-- WebSocket 探测
|
||||
local result = ""
|
||||
local success = false
|
||||
-- WebSocket 探测 (适用于域名,或带 SNI 的 IP)
|
||||
if not is_ip or (host and host ~= "") then
|
||||
local resolve_arg = ""
|
||||
local final_domain = domain
|
||||
if is_ip and host and host ~= "" then
|
||||
-- IP 模式下使用 --resolve 强制指定 SNI,解决 TLS 握手失败
|
||||
resolve_arg = string.format("--resolve '%s:%d:%s' ", host, port, domain)
|
||||
final_domain = host
|
||||
end
|
||||
local prefix = (tls == '1') and "https://" or "http://"
|
||||
local address = prefix .. final_domain .. ':' .. port .. wsPath
|
||||
local cmd = string.format(
|
||||
"curl --http1.1 -m 2 -ksN -o /dev/null %s" ..
|
||||
"-w 'time_connect=%%{time_connect}\\nhttp_code=%%{http_code}' " ..
|
||||
"-H 'Connection: Upgrade' -H 'Upgrade: websocket' " ..
|
||||
"-H 'Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==' " ..
|
||||
"-H 'Sec-WebSocket-Version: 13' '%s'",
|
||||
resolve_arg, address
|
||||
)
|
||||
result = luci.sys.exec(cmd) or ""
|
||||
success = (string.match(result, "http_code=(%d+)") == "101")
|
||||
end
|
||||
-- 如果深度探测失败,或是不支持深测的纯 IP
|
||||
if not success then
|
||||
local socket = nixio.socket("inet", "stream")
|
||||
if socket then
|
||||
socket:setopt("socket", "rcvtimeo", 3)
|
||||
socket:setopt("socket", "sndtimeo", 3)
|
||||
success = socket:connect(domain, port)
|
||||
socket:close()
|
||||
end
|
||||
--luci.sys.exec(string.format("echo 'Node %s (ws) failed deep test, using TCP fallback' >> /tmp/ping.log", domain))
|
||||
end
|
||||
e.socket = success
|
||||
-- 解析延迟 (优先用 curl 数据,失败则回退到 tcping)
|
||||
local ping_time = tonumber(string.match(result, "time_connect=(%d+.%d%d%d)"))
|
||||
if ping_time and ping_time > 0 then
|
||||
e.ping = math.floor(ping_time * 1000)
|
||||
else
|
||||
local tcping_cmd = string.format("tcping -q -c 1 -t 1 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | cut -d= -f2", port, domain)
|
||||
e.ping = tonumber(luci.sys.exec(tcping_cmd)) or 0
|
||||
end
|
||||
else
|
||||
-- 3. 非 WebSocket 节点的探测逻辑 (TCP / ICMP / UDP)
|
||||
local socket = nixio.socket("inet", "stream")
|
||||
if socket then
|
||||
socket:setopt("socket", "rcvtimeo", 3)
|
||||
socket:setopt("socket", "sndtimeo", 3)
|
||||
e.socket = socket:connect(domain, port)
|
||||
socket:close()
|
||||
end
|
||||
|
||||
if transport == "ws" then
|
||||
local prefix = tls == '1' and "https://" or "http://"
|
||||
local address = prefix .. domain .. ':' .. port .. wsPath
|
||||
local result = luci.sys.exec(
|
||||
"curl --http1.1 -m 2 -ksN -o /dev/null " ..
|
||||
"-w 'time_connect=%{time_connect}\nhttp_code=%{http_code}' " ..
|
||||
"-H 'Connection: Upgrade' -H 'Upgrade: websocket' " ..
|
||||
"-H 'Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==' " ..
|
||||
"-H 'Sec-WebSocket-Version: 13' " .. address
|
||||
)
|
||||
e.socket = string.match(result,"http_code=(%d+)") == "101"
|
||||
local ping_time = tonumber(string.match(result, "time_connect=(%d+.%d%d%d)"))
|
||||
e.ping = ping_time and ping_time * 1000 or nil
|
||||
else
|
||||
-- TCP ping
|
||||
local socket = nixio.socket("inet", "stream")
|
||||
socket:setopt("socket", "rcvtimeo", 3)
|
||||
socket:setopt("socket", "sndtimeo", 3)
|
||||
e.socket = socket:connect(domain, port)
|
||||
socket:close()
|
||||
-- 延迟:tcping -> ping -> nping(udp)
|
||||
local tcping_cmd = string.format("tcping -q -c 1 -t 1 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | cut -d= -f2", port, domain)
|
||||
e.ping = tonumber(luci.sys.exec(tcping_cmd))
|
||||
if not e.ping then
|
||||
local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain)
|
||||
e.ping = tonumber(luci.sys.exec(icmp_cmd))
|
||||
end
|
||||
|
||||
e.ping = tonumber(luci.sys.exec(string.format(
|
||||
"tcping -q -c 1 -i 1 -t 2 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}'",
|
||||
port, domain
|
||||
)))
|
||||
if not e.ping then
|
||||
local udp_cmd = string.format("nping --udp -c 1 -p %d %s 2>/dev/null | grep -o 'Avg rtt: [0-9.]*ms' | awk '{print $3}' | sed 's/ms//' | head -1", port, domain)
|
||||
local udp_res = luci.sys.exec(udp_cmd)
|
||||
if udp_res and udp_res ~= "" then
|
||||
local ping_num = tonumber(udp_res)
|
||||
if ping_num then e.ping = math.floor(ping_num) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not e.ping then
|
||||
e.ping = tonumber(luci.sys.exec(string.format(
|
||||
"ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}'",
|
||||
domain
|
||||
)))
|
||||
end
|
||||
-- 4. 清理防火墙规则
|
||||
if iret then
|
||||
if use_nft then
|
||||
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null")
|
||||
else
|
||||
luci.sys.call("ipset del ss_spec_wan_ac " .. domain .. " 2>/dev/null")
|
||||
end
|
||||
end
|
||||
|
||||
if not e.ping then
|
||||
local ping_result = tonumber(luci.sys.exec(string.format(
|
||||
"nping --udp -c 1 -p %d %s 2>/dev/null | grep -o 'Avg rtt: [0-9.]*ms' | awk '{print $3}' | sed 's/ms//' | head -1",
|
||||
port, domain
|
||||
)))
|
||||
local ping_num = tonumber(ping_result)
|
||||
if ping_num then
|
||||
e.ping = math.floor(ping_num)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if iret then
|
||||
if use_nft then
|
||||
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null")
|
||||
else
|
||||
luci.sys.call("ipset del ss_spec_wan_ac " .. domain .. " 2>/dev/null")
|
||||
end
|
||||
end
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
function check_status()
|
||||
@ -246,15 +347,109 @@ function act_reset()
|
||||
end
|
||||
|
||||
function act_restart()
|
||||
luci.sys.call("/etc/init.d/shadowsocksr restart &")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr"))
|
||||
end
|
||||
|
||||
function act_delete()
|
||||
luci.sys.call("/etc/init.d/shadowsocksr restart &")
|
||||
uci:delete_all("shadowsocksr", "servers", function(s)
|
||||
if s.hashkey or s.isSubscribe then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
uci:commit("shadowsocksr")
|
||||
for file in nixio.fs.glob("/tmp/sub_md5_*") do
|
||||
nixio.fs.remove(file)
|
||||
end
|
||||
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
|
||||
end
|
||||
|
||||
function act_add_node()
|
||||
local redirect = luci.http.formvalue("redirect")
|
||||
local used_sid = {}
|
||||
local next_sid = 1
|
||||
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
local num = s[".name"]:match("^cfg(%x%x)")
|
||||
if num then
|
||||
local n = tonumber(num, 16)
|
||||
used_sid[n] = true
|
||||
end
|
||||
end)
|
||||
|
||||
local function get_next_sid()
|
||||
while used_sid[next_sid] do
|
||||
next_sid = next_sid + 1
|
||||
end
|
||||
used_sid[next_sid] = true
|
||||
return next_sid
|
||||
end
|
||||
|
||||
local sid = uci:section("shadowsocksr", "servers", nil)
|
||||
local suffix = sid:sub(-4)
|
||||
uci:delete("shadowsocksr", sid)
|
||||
|
||||
local id = get_next_sid()
|
||||
local cfgid = string.format("cfg%02x%s", id, suffix)
|
||||
uci:section("shadowsocksr", "servers", cfgid)
|
||||
uci_save(uci, "shadowsocksr")
|
||||
|
||||
if redirect == "1" then
|
||||
luci.http.redirect(url("servers", cfgid))
|
||||
else
|
||||
luci.http.write_json({ result = cfgid })
|
||||
end
|
||||
end
|
||||
|
||||
function act_remove()
|
||||
local id = luci.http.formvalue("id")
|
||||
if id then
|
||||
uci:delete("shadowsocksr", id)
|
||||
uci:commit("shadowsocksr")
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
|
||||
end
|
||||
|
||||
function act_save_order()
|
||||
local ids = luci.http.formvalue("ids") or ""
|
||||
local new_order = {}
|
||||
for id in ids:gmatch("([^,]+)") do
|
||||
new_order[#new_order + 1] = id
|
||||
end
|
||||
|
||||
for idx, name in ipairs(new_order) do
|
||||
luci.sys.call(string.format("uci -q reorder %s.%s=%d", "shadowsocksr", name, idx - 1))
|
||||
end
|
||||
|
||||
sh_uci_commit("shadowsocksr")
|
||||
luci.http.write_json({ status = "ok" })
|
||||
end
|
||||
|
||||
function act_get_now_use_node()
|
||||
local result = {}
|
||||
local tcp_node = uci:get_first("shadowsocksr", "global", "global_server")
|
||||
if tcp_node then
|
||||
result["TCP"] = tcp_node
|
||||
end
|
||||
local udp_node = uci:get_first("shadowsocksr", "global", "udp_relay_server")
|
||||
if udp_node then
|
||||
result["UDP"] = udp_node
|
||||
end
|
||||
local netflix_node = uci:get_first("shadowsocksr", "global", "netflix_server")
|
||||
if netflix_node then
|
||||
result["netflix"] = netflix_node
|
||||
end
|
||||
local socks5_node = uci:get_first("shadowsocksr", "socks5_proxy", "server")
|
||||
if socks5_node then
|
||||
result["socks5"] = socks5_node
|
||||
end
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(result)
|
||||
end
|
||||
|
||||
function get_log()
|
||||
luci.http.write(luci.sys.exec("[ -f '/var/log/ssrplus.log' ] && cat /var/log/ssrplus.log"))
|
||||
end
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
local m, s, o
|
||||
local cbi = require "luci.cbi"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
-- 获取 LAN IP 地址
|
||||
@ -275,33 +276,33 @@ o.rmempty = false
|
||||
o = s:option(ListValue, "server", translate("Server"))
|
||||
o:value("same", translate("Same as Global Server"))
|
||||
for _, key in pairs(key_table) do
|
||||
o:value(key, server_table[key])
|
||||
o:value(key, server_table[key])
|
||||
end
|
||||
o.default = "same"
|
||||
o.rmempty = false
|
||||
|
||||
-- Dynamic value handling based on enabled/disabled state
|
||||
o.cfgvalue = function(self, section)
|
||||
local enabled = m:get(section, "enabled")
|
||||
if enabled == "0" then
|
||||
return m:get(section, "old_server")
|
||||
end
|
||||
return Value.cfgvalue(self, section)-- Default to `same` when enabled
|
||||
local enabled = m:get(section, "enabled")
|
||||
if enabled == "0" then
|
||||
return m:get(section, "old_server")
|
||||
end
|
||||
return Value.cfgvalue(self, section)-- Default to `same` when enabled
|
||||
end
|
||||
|
||||
o.write = function(self, section, value)
|
||||
local enabled = m:get(section, "enabled")
|
||||
if enabled == "0" then
|
||||
local old_server = Value.cfgvalue(self, section)
|
||||
if old_server ~= "nil" then
|
||||
m:set(section, "old_server", old_server)
|
||||
end
|
||||
m:set(section, "server", "nil")
|
||||
else
|
||||
m:del(section, "old_server")
|
||||
-- Write the value normally when enabled
|
||||
Value.write(self, section, value)
|
||||
end
|
||||
local enabled = m:get(section, "enabled")
|
||||
if enabled == "0" then
|
||||
local old_server = Value.cfgvalue(self, section)
|
||||
if old_server ~= "nil" then
|
||||
m:set(section, "old_server", old_server)
|
||||
end
|
||||
m:set(section, "server", "nil")
|
||||
else
|
||||
m:del(section, "old_server")
|
||||
-- Write the value normally when enabled
|
||||
Value.write(self, section, value)
|
||||
end
|
||||
end
|
||||
|
||||
-- Socks Auth
|
||||
@ -312,10 +313,10 @@ o:value("noauth", "NOAUTH")
|
||||
o:value("password", "PASSWORD")
|
||||
o.rmempty = true
|
||||
for key, server_type in pairs(type_table) do
|
||||
if server_type == "v2ray" then
|
||||
-- 如果服务器类型是 v2ray,则设置依赖项显示
|
||||
o:depends("server", key)
|
||||
end
|
||||
if server_type == "v2ray" then
|
||||
-- 如果服务器类型是 v2ray,则设置依赖项显示
|
||||
o:depends("server", key)
|
||||
end
|
||||
end
|
||||
o:depends({server = "same", disable = true})
|
||||
|
||||
@ -335,10 +336,10 @@ o = s:option(Flag, "socks5_mixed", translate("Enabled Mixed"), translate("Mixed
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
for key, server_type in pairs(type_table) do
|
||||
if server_type == "v2ray" then
|
||||
-- 如果服务器类型是 v2ray,则设置依赖项显示
|
||||
o:depends("server", key)
|
||||
end
|
||||
if server_type == "v2ray" then
|
||||
-- 如果服务器类型是 v2ray,则设置依赖项显示
|
||||
o:depends("server", key)
|
||||
end
|
||||
end
|
||||
o:depends({server = "same", disable = true})
|
||||
end
|
||||
@ -367,17 +368,15 @@ if is_finded("xray") then
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s:option(Value, "fragment_length", translate("Fragment Length"), translate("Fragmented packet length (byte)"))
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.default = "100-200"
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s:option(Value, "fragment_interval", translate("Fragment Interval"), translate("Fragmentation interval (ms)"))
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.default = "10-20"
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s:option(Value, "fragment_maxsplit", translate("Max Split"), translate("Limit the maximum number of splits."))
|
||||
o.default = "100-200"
|
||||
o:depends("fragment", true)
|
||||
|
||||
o = s:option(Flag, "noise", translate("Noise"), translate("UDP noise, Under some circumstances it can bypass some UDP based protocol restrictions."))
|
||||
o.default = 0
|
||||
|
||||
@ -427,13 +426,8 @@ if is_finded("xray") then
|
||||
o = s:option(Value, "delay", translate("Delay (ms)"))
|
||||
o.datatype = "or(uinteger,portrange)"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "applyto", translate("IP Type"))
|
||||
o.default = "IP"
|
||||
o:value("IP", "ALL")
|
||||
o:value("IPV4", "IPv4")
|
||||
o:value("IPV6", "IPv6")
|
||||
o.rmempty = false
|
||||
|
||||
s:append(cbi.Template("shadowsocksr/optimize_cbi_ui"))
|
||||
end
|
||||
|
||||
return m
|
||||
|
||||
@ -12,6 +12,8 @@ local m, s, o
|
||||
|
||||
local sid = arg[1]
|
||||
local uuid = luci.sys.exec("cat /proc/sys/kernel/random/uuid")
|
||||
local b64decode = nixio.bin.b64decode
|
||||
local b64encode = nixio.bin.b64encode
|
||||
local xray_version = nil
|
||||
local xray_version_val = 0
|
||||
|
||||
@ -24,11 +26,59 @@ local function is_installed(e)
|
||||
return luci.model.ipkg.installed(e)
|
||||
end
|
||||
|
||||
local function is_js_luci()
|
||||
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
|
||||
end
|
||||
|
||||
-- trim
|
||||
local function trim(text)
|
||||
if not text or text == "" then
|
||||
return ""
|
||||
end
|
||||
return (text:gsub("^%s*(.-)%s*$", "%1"))
|
||||
end
|
||||
|
||||
-- base64
|
||||
local function base64Decode(text)
|
||||
local raw = text
|
||||
if not text or text == "" then
|
||||
return ''
|
||||
end
|
||||
text = text:gsub("%z", "")
|
||||
text = text:gsub("_", "/")
|
||||
text = text:gsub("-", "+")
|
||||
local mod4 = #text % 4
|
||||
text = text .. string.sub('====', mod4 + 1)
|
||||
local result = b64decode(text)
|
||||
if result then
|
||||
return result:gsub("%z", "")
|
||||
else
|
||||
return raw
|
||||
end
|
||||
end
|
||||
|
||||
local function base64Encode(text)
|
||||
if not text or text == "" then
|
||||
return ''
|
||||
end
|
||||
local result = b64encode(text)
|
||||
if result then
|
||||
result = result:gsub("%z", "")
|
||||
result = result:gsub("/", "_")
|
||||
result = result:gsub("+", "-")
|
||||
result = result:gsub("=", "")
|
||||
return result
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
-- 获取 Xray 版本号
|
||||
if is_finded("xray") then
|
||||
local version = luci.sys.exec("xray version 2>&1")
|
||||
if version and version ~= "" then
|
||||
xray_version = version:match("Xray%s+([%d%.]+)")
|
||||
-- xray_version = version:match("([0-9]+%.[0-9]+%.[0-9]+)")
|
||||
end
|
||||
end
|
||||
|
||||
@ -44,6 +94,17 @@ if xray_version and xray_version ~= "" then
|
||||
xray_version_val = major * 10000 + minor * 100 + patch
|
||||
end
|
||||
|
||||
local function url(...)
|
||||
local url = string.format("admin/services/%s", "shadowsocksr")
|
||||
local args = { ... }
|
||||
for i, v in ipairs(args) do
|
||||
if v and v ~= "" then
|
||||
url = url .. "/" .. v
|
||||
end
|
||||
end
|
||||
return require "luci.dispatcher".build_url(url)
|
||||
end
|
||||
|
||||
-- 默认的保存并应用行为
|
||||
local function apply_redirect(m)
|
||||
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
|
||||
@ -64,7 +125,7 @@ local function apply_redirect(m)
|
||||
m.on_after_save = function(self)
|
||||
local redirect = self.redirect
|
||||
if redirect and redirect ~= "" then
|
||||
uci:set("shadowsocksr" .. "_redirect", "@redirect[0]", "url", redirect)
|
||||
m.uci:set("shadowsocksr" .. "_redirect", "@redirect[0]", "url", redirect)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -72,18 +133,24 @@ local function apply_redirect(m)
|
||||
end
|
||||
end
|
||||
|
||||
local function set_apply_on_parse(map)
|
||||
if not map then return end
|
||||
if is_js_luci() then
|
||||
apply_redirect(map)
|
||||
local old = map.on_after_save
|
||||
map.on_after_save = function(self)
|
||||
if old then old(self) end
|
||||
map:set("@global[0]", "timestamp", os.time())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local has_xray = is_finded("xray")
|
||||
local has_hysteria2 = is_finded("hysteria")
|
||||
|
||||
-- 读取当前存储的 xray_hy2_type
|
||||
local xray_hy2_type = uci:get_first("shadowsocksr", "server_subscribe", "xray_hy2_type")
|
||||
|
||||
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
|
||||
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
|
||||
|
||||
-- 读取当前存储的 ss_type
|
||||
local ss_type = uci:get_first("shadowsocksr", "server_subscribe", "ss_type")
|
||||
|
||||
local server_table = {}
|
||||
local encrypt_methods = {
|
||||
-- ssr
|
||||
@ -188,13 +255,13 @@ local tls_flows = {
|
||||
}
|
||||
|
||||
m = Map("shadowsocksr", translate("Edit ShadowSocksR Server"))
|
||||
m.redirect = luci.dispatcher.build_url("admin/services/shadowsocksr/servers")
|
||||
if m.uci:get("shadowsocksr", sid) ~= "servers" then
|
||||
m.redirect = url("servers")
|
||||
if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then
|
||||
luci.http.redirect(m.redirect)
|
||||
return
|
||||
end
|
||||
-- 保存&应用成功后跳转到节点列表
|
||||
apply_redirect(m)
|
||||
set_apply_on_parse(m)
|
||||
|
||||
-- [[ Servers Setting ]]--
|
||||
s = m:section(NamedSection, sid, "servers")
|
||||
@ -207,40 +274,27 @@ o.template = "shadowsocksr/ssrurl"
|
||||
o.value = sid
|
||||
|
||||
-- 新增一个选择框,用于选择 Xray 或 Hysteria2 核心
|
||||
o = s:option(ListValue, "xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
|
||||
o = s:option(ListValue, "_xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
|
||||
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
|
||||
-- 设置默认 Xray 或 Hysteria2 核心
|
||||
-- 动态添加选项
|
||||
if has_xray then
|
||||
o:value("xray", translate("Xray"))
|
||||
o:value("v2ray", translate("Xray (Hysteria2)"))
|
||||
end
|
||||
if has_hysteria2 then
|
||||
o:value("hysteria2", translate("Hysteria2"))
|
||||
end
|
||||
-- 设置默认值
|
||||
if xray_hy2_type == "xray" then
|
||||
o.default = "xray"
|
||||
elseif xray_hy2_type == "hysteria2" then
|
||||
o.default = "hysteria2"
|
||||
-- 读取全局 xray_hy2_type
|
||||
o.cfgvalue = function(self, section)
|
||||
return self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type") or "hysteria2"
|
||||
end
|
||||
o.rmempty = false
|
||||
o.write = function(self, section, value)
|
||||
-- 更新 Hysteria 节点的 xray_hy2_type
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
local node_type = uci:get("shadowsocksr", s[".name"], "type") -- 获取节点类型
|
||||
if node_type == "hysteria2" then -- 仅修改 Hysteria 节点
|
||||
local old_value = uci:get("shadowsocksr", s[".name"], "xray_hy2_type")
|
||||
if old_value ~= value then
|
||||
uci:set("shadowsocksr", s[".name"], "xray_hy2_type", value)
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- 更新 server_subscribe 的 xray_hy2_type
|
||||
local old_value = uci:get("shadowsocksr", "server_subscribe", "xray_hy2_type")
|
||||
local old_value = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
|
||||
if old_value ~= value then
|
||||
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
|
||||
self.map.uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
|
||||
end
|
||||
-- 更新当前 section 的 xray_hy2_type
|
||||
ListValue.write(self, section, value)
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "type", translate("Server Node Type"))
|
||||
@ -289,7 +343,7 @@ o:depends("type", "tun")
|
||||
o.description = translate("Redirect traffic to this network interface")
|
||||
|
||||
-- 新增一个选择框,用于选择 Shadowsocks 版本
|
||||
o = s:option(ListValue, "has_ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
|
||||
o = s:option(ListValue, "_has_ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
|
||||
o.description = translate("Selection ShadowSocks Node Use Version.")
|
||||
-- 设置默认 Shadowsocks 版本
|
||||
-- 动态添加选项
|
||||
@ -299,31 +353,18 @@ end
|
||||
if has_ss_libev then
|
||||
o:value("ss-libev", translate("ShadowSocks-libev Version"))
|
||||
end
|
||||
-- 设置默认值
|
||||
if ss_type == "ss-rust" then
|
||||
o.default = "ss-rust"
|
||||
elseif ss_type == "ss-libev" then
|
||||
o.default = "ss-libev"
|
||||
-- 读取全局 ss_type
|
||||
o.cfgvalue = function(self, section)
|
||||
return self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type") or "ss-rust"
|
||||
end
|
||||
o:depends("type", "ss")
|
||||
o.rmempty = false
|
||||
o.write = function(self, section, value)
|
||||
-- 更新 Shadowsocks 节点的 has_ss_type
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
local node_type = uci:get("shadowsocksr", s[".name"], "type") -- 获取节点类型
|
||||
if node_type == "ss" then -- 仅修改 Shadowsocks 节点
|
||||
local old_value = uci:get("shadowsocksr", s[".name"], "has_ss_type")
|
||||
if old_value ~= value then
|
||||
uci:set("shadowsocksr", s[".name"], "has_ss_type", value)
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- 更新 server_subscribe 的 ss_type
|
||||
local old_value = uci:get("shadowsocksr", "server_subscribe", "ss_type")
|
||||
local old_value = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type")
|
||||
if old_value ~= value then
|
||||
uci:set("shadowsocksr", "@server_subscribe[0]", "ss_type", value)
|
||||
self.map.uci:set("shadowsocksr", "@server_subscribe[0]", "ss_type", value)
|
||||
end
|
||||
-- 更新当前 section 的 has_ss_type
|
||||
ListValue.write(self, section, value)
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol"))
|
||||
@ -851,6 +892,11 @@ o = s:option(Value, "ws_path", translate("WebSocket Path"))
|
||||
o:depends("transport", "ws")
|
||||
o.rmempty = true
|
||||
|
||||
-- WS间隔
|
||||
o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)"))
|
||||
o.datatype = "integer"
|
||||
o:depends("transport", "ws")
|
||||
|
||||
if is_finded("v2ray") then
|
||||
-- WS前置数据
|
||||
o = s:option(Value, "ws_ed", translate("Max Early Data"))
|
||||
@ -910,39 +956,47 @@ o:depends("transport", "xhttp")
|
||||
|
||||
o = s:option(TextValue, "xhttp_extra", " ")
|
||||
o.description = translate(
|
||||
"<font><b>" .. translate("Configure XHTTP Extra Settings (JSON format), see:") .. "</b></font>" ..
|
||||
" <a href='https://xtls.github.io/config/transports/splithttp.html#extra' target='_blank'>" ..
|
||||
"<font style='color:green'><b>" .. translate("Click to the page") .. "</b></font></a>")
|
||||
"<font><b>" .. translate("Configure XHTTP Extra Settings (JSON format), see:") .. "</b></font>" ..
|
||||
" <a href='https://xtls.github.io/config/transports/splithttp.html#extra' target='_blank'>" ..
|
||||
"<font style='color:green'><b>" .. translate("Click to the page") .. "</b></font></a>")
|
||||
o:depends("enable_xhttp_extra", true)
|
||||
o.rmempty = true
|
||||
--o.rmempty = true
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o.custom_write = function(self, section, value)
|
||||
m:set(section, "xhttp_extra", value)
|
||||
local success, data = pcall(luci.jsonc.parse, value)
|
||||
if success and data then
|
||||
local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address)
|
||||
or (data.downloadSettings and data.downloadSettings.address)
|
||||
if address and address ~= "" then
|
||||
m:set(section, "download_address", address)
|
||||
else
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
else
|
||||
o.cfgvalue = function(self, section, value)
|
||||
local raw = m:get(section, "xhttp_extra")
|
||||
if raw then
|
||||
return base64Decode(raw)
|
||||
end
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
m:set(section, "xhttp_extra", base64Encode(value) or "")
|
||||
local success, data = pcall(luci.jsonc.parse, value)
|
||||
if success and data then
|
||||
local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address)
|
||||
or (data.downloadSettings and data.downloadSettings.address)
|
||||
if address and address ~= "" then
|
||||
address = address:gsub("^%[", ""):gsub("%]$", "")
|
||||
m:set(section, "download_address", address)
|
||||
else
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
else
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
end
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = value:gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||
if value:sub(-1) == "\n" then
|
||||
value = value:sub(1, -2)
|
||||
end
|
||||
local success, data = pcall(luci.jsonc.parse, value)
|
||||
if not success or not data then
|
||||
return nil, translate("Invalid JSON format")
|
||||
end
|
||||
|
||||
return value
|
||||
value = trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||
local ok, data = pcall(luci.jsonc.parse, value)
|
||||
if ok and data then
|
||||
return value
|
||||
else
|
||||
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
|
||||
end
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
m:del(section, "xhttp_extra")
|
||||
m:del(section, "download_address")
|
||||
end
|
||||
|
||||
-- [[ H2部分 ]]--
|
||||
@ -1097,7 +1151,12 @@ o.rmempty = true
|
||||
|
||||
-- [[ WireGuard 部分 ]]--
|
||||
o = s:option(Flag, "kernelmode", translate("Enabled Kernel virtual NIC TUN(optional)"))
|
||||
o.description = translate("Virtual NIC TUN of Linux kernel can be used only when system supports and have root permission. If used, IPv6 routing table 1023 is occupied.")
|
||||
o.description = translate(
|
||||
"<ul>" ..
|
||||
"<li>" .. translate("Linux kernel TUN virtual NIC requires system support and root privileges.") .. "</li>" ..
|
||||
"<li>" .. translate("When enabled, it occupies IPv6 routing table 1023.") .. "</li>" ..
|
||||
"</ul>"
|
||||
)
|
||||
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
|
||||
o.default = "0"
|
||||
o.rmempty = true
|
||||
@ -1108,7 +1167,7 @@ o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(DynamicList, "reserved", translate("Reserved bytes(optional)"))
|
||||
o.description = translate("Wireguard reserved bytes.")
|
||||
o.description = translate("Decimal numbers separated by \",\" or Base64-encoded strings.")
|
||||
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
|
||||
o.rmempty = true
|
||||
|
||||
@ -1133,6 +1192,48 @@ o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
|
||||
o.default = "0.0.0.0/0"
|
||||
o.rmempty = true
|
||||
|
||||
-- [[ User-Agent部分 ]]--
|
||||
o = s:option(Value, "user_agent", translate("User-Agent"))
|
||||
o:depends("tcp_guise", "http")
|
||||
o:depends("transport", "ws")
|
||||
o:depends("transport", "httpupgrade")
|
||||
o:depends("transport", "xhttp")
|
||||
o:depends("transport", "grpc")
|
||||
|
||||
--[[ FinalMask部分 ]]--
|
||||
o = s:option(Flag, "enable_finalmask", translate("FinalMask"))
|
||||
o.rmempty = true
|
||||
o.default = "0"
|
||||
o:depends({type = "v2ray", v2ray_protocol = "vless"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "vmess"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "trojan"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
|
||||
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
|
||||
|
||||
o = s:option(TextValue, "finalmask", " ")
|
||||
o.description = translate("An FinalMaskObject in JSON format, used for sharing.")
|
||||
o:depends("enable_finalmask", true)
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o.custom_cfgvalue = function(self, section, value)
|
||||
local raw = m:get(section, "finalmask")
|
||||
if raw then
|
||||
return base64Decode(raw)
|
||||
end
|
||||
end
|
||||
o.custom_write = function(self, section, value)
|
||||
m:set(section, "finalmask", base64Encode(value) or "")
|
||||
end
|
||||
o.validate = function(self, value)
|
||||
value = trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
|
||||
if luci.jsonc.parse(value) then
|
||||
return value
|
||||
else
|
||||
return nil, "FinalMask " .. translate("Must be JSON text!")
|
||||
end
|
||||
end
|
||||
|
||||
-- [[ TLS ]]--
|
||||
o = s:option(Flag, "tls", translate("TLS"))
|
||||
o.rmempty = true
|
||||
@ -1297,6 +1398,7 @@ o:depends("type", "trojan")
|
||||
o:depends("type", "tuic")
|
||||
o.description = translate("If true, allowss insecure connection at TLS client, e.g., TLS server uses unverifiable certificates.")
|
||||
-- Xray 支持时间判断
|
||||
-- if os.time() < os.time({year=2026,month=6,day=1}) then
|
||||
if os.date("%Y.%m.%d") < "2026.06.01" then
|
||||
-- Xray 支持到 26.06.01
|
||||
o:depends("tls", true)
|
||||
|
||||
@ -15,24 +15,79 @@ local function is_finded(e)
|
||||
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
|
||||
end
|
||||
|
||||
local function is_js_luci()
|
||||
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
|
||||
end
|
||||
|
||||
local function url(...)
|
||||
local url = string.format("admin/services/%s", "shadowsocksr")
|
||||
local args = { ... }
|
||||
for i, v in ipairs(args) do
|
||||
if v and v ~= "" then
|
||||
url = url .. "/" .. v
|
||||
end
|
||||
end
|
||||
return require "luci.dispatcher".build_url(url)
|
||||
end
|
||||
|
||||
-- 默认的保存并应用行为
|
||||
local function apply_redirect(m)
|
||||
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
|
||||
if m.redirect and m.redirect ~= "" then
|
||||
if nixio.fs.access(tmp_uci_file) then
|
||||
local redirect
|
||||
for line in io.lines(tmp_uci_file) do
|
||||
redirect = line:match("option%s+url%s+['\"]([^'\"]+)['\"]")
|
||||
if redirect and redirect ~= "" then break end
|
||||
end
|
||||
if redirect and redirect ~= "" then
|
||||
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
|
||||
luci.http.redirect(redirect)
|
||||
end
|
||||
else
|
||||
nixio.fs.writefile(tmp_uci_file, "config redirect\n")
|
||||
end
|
||||
m.on_after_save = function(self)
|
||||
local redirect = self.redirect
|
||||
if redirect and redirect ~= "" then
|
||||
m.uci:set("shadowsocksr" .. "_redirect", "@redirect[0]", "url", redirect)
|
||||
end
|
||||
end
|
||||
else
|
||||
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
|
||||
end
|
||||
end
|
||||
|
||||
local function set_apply_on_parse(map)
|
||||
if not map then return end
|
||||
if is_js_luci() then
|
||||
apply_redirect(map)
|
||||
local old = map.on_after_save
|
||||
map.on_after_save = function(self)
|
||||
if old then old(self) end
|
||||
map:set("@global[0]", "timestamp", os.time())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local has_xray = is_finded("xray")
|
||||
local has_hysteria2 = is_finded("hysteria")
|
||||
|
||||
local hy2_type_list = {}
|
||||
|
||||
if has_xray then
|
||||
table.insert(hy2_type_list, { id = "xray", name = translate("Xray") })
|
||||
end
|
||||
if has_hysteria2 then
|
||||
table.insert(hy2_type_list, { id = "hysteria2", name = translate("Hysteria2") })
|
||||
end
|
||||
if has_xray then
|
||||
table.insert(hy2_type_list, { id = "v2ray", name = translate("Xray (Hysteria2)") })
|
||||
end
|
||||
|
||||
-- 如果用户没有手动设置,则自动选择
|
||||
if not xray_hy2_type or xray_hy2_type == "" then
|
||||
if has_hysteria2 then
|
||||
xray_hy2_type = "hysteria2"
|
||||
elseif has_xray then
|
||||
xray_hy2_type = "xray"
|
||||
xray_hy2_type = "v2ray"
|
||||
end
|
||||
end
|
||||
|
||||
@ -47,6 +102,9 @@ end
|
||||
if has_ss_libev then
|
||||
table.insert(ss_type_list, { id = "ss-libev", name = translate("ShadowSocks-libev Version") })
|
||||
end
|
||||
if has_xray then
|
||||
table.insert(ss_type_list, { id = "v2ray", name = translate("Xray (ShadowSocks)") })
|
||||
end
|
||||
|
||||
-- 如果用户没有手动设置,则自动选择
|
||||
if not ss_type or ss_type == "" then
|
||||
@ -54,6 +112,8 @@ if not ss_type or ss_type == "" then
|
||||
ss_type = "ss-rust"
|
||||
elseif has_ss_libev then
|
||||
ss_type = "ss-libev"
|
||||
elseif has_xray then
|
||||
ss_type = "v2ray"
|
||||
end
|
||||
end
|
||||
|
||||
@ -108,20 +168,6 @@ if #hy2_type_list > 0 then
|
||||
o:value(v.id, v.name) -- 存储 "Xray" / "Hysteria2",但 UI 显示完整名称
|
||||
end
|
||||
o.default = xray_hy2_type -- 设置默认值
|
||||
o.write = function(self, section, value)
|
||||
-- 更新 Hysteria 节点的 xray_hy2_type
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
local node_type = uci:get("shadowsocksr", s[".name"], "type") -- 获取节点类型
|
||||
if node_type == "hysteria2" then -- 仅修改 Hysteria 节点
|
||||
local old_value = uci:get("shadowsocksr", s[".name"], "xray_hy2_type")
|
||||
if old_value ~= value then
|
||||
uci:set("shadowsocksr", s[".name"], "xray_hy2_type", value)
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- 更新当前 section 的 xray_hy2_type
|
||||
ListValue.write(self, section, value)
|
||||
end
|
||||
end
|
||||
|
||||
-- 确保 ss_type_list 不为空
|
||||
@ -132,20 +178,6 @@ if #ss_type_list > 0 then
|
||||
o:value(v.id, v.name) -- 存储 "ss-libev" / "ss-rust",但 UI 显示完整名称
|
||||
end
|
||||
o.default = ss_type -- 设置默认值
|
||||
o.write = function(self, section, value)
|
||||
-- 更新 Shadowsocks 节点的 has_ss_type
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
local node_type = uci:get("shadowsocksr", s[".name"], "type") -- 获取节点类型
|
||||
if node_type == "ss" then -- 仅修改 Shadowsocks 节点
|
||||
local old_value = uci:get("shadowsocksr", s[".name"], "has_ss_type")
|
||||
if old_value ~= value then
|
||||
uci:set("shadowsocksr", s[".name"], "has_ss_type", value)
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- 更新当前 section 的 ss_type
|
||||
ListValue.write(self, section, value)
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(DynamicList, "subscribe_url", translate("Subscribe URL"))
|
||||
@ -190,22 +222,19 @@ o = s:option(Button, "delete", translate("Delete All Subscribe Servers"))
|
||||
o.inputstyle = "reset"
|
||||
o.description = string.format(translate("Server Count") .. ": %d", server_count)
|
||||
o.write = function()
|
||||
uci:delete_all("shadowsocksr", "servers", function(s)
|
||||
if s.hashkey or s.isSubscribe then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
uci:save("shadowsocksr")
|
||||
uci:commit("shadowsocksr")
|
||||
for file in nixio.fs.glob("/tmp/sub_md5_*") do
|
||||
nixio.fs.remove(file)
|
||||
end
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete"))
|
||||
return
|
||||
luci.http.redirect(url("delete"))
|
||||
end
|
||||
|
||||
o = s:option(Value, "url_test_url", translate("URL Test Address"))
|
||||
o:value("https://cp.cloudflare.com/", "Cloudflare")
|
||||
o:value("https://www.gstatic.com/generate_204", "Gstatic")
|
||||
o:value("https://www.google.com/generate_204", "Google")
|
||||
o:value("https://www.youtube.com/generate_204", "YouTube")
|
||||
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||
o.default = o.keylist[3]
|
||||
|
||||
|
||||
o = s:option(Value, "user_agent", translate("User-Agent"))
|
||||
o.default = "v2rayN/9.99"
|
||||
o:value("curl", "Curl")
|
||||
@ -218,16 +247,23 @@ s = m:section(TypedSection, "servers")
|
||||
s.anonymous = true
|
||||
s.addremove = true
|
||||
s.template = "cbi/tblsection"
|
||||
s:append(cbi.Template("shadowsocksr" .. "/optimize_cbi_ui"))
|
||||
set_apply_on_parse(m)
|
||||
s.sortable = true
|
||||
s.extedit = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers", "%s")
|
||||
function s.create(...)
|
||||
local sid = TypedSection.create(...)
|
||||
if sid then
|
||||
luci.http.redirect(s.extedit % sid)
|
||||
return
|
||||
end
|
||||
--[[
|
||||
s.extedit = url("servers", "%s")
|
||||
function s.create(self, ...)
|
||||
local sid = TypedSection.create(self, ...)
|
||||
if sid then
|
||||
local newsid = "cfg" .. sid:sub(-6)
|
||||
-- 删除匿名
|
||||
self.map.uci:delete(self.config, sid)
|
||||
-- 重命名 section
|
||||
self.map.uci:section(self.config, self.sectiontype, newsid)
|
||||
luci.http.redirect(self.extedit % newsid)
|
||||
return
|
||||
end
|
||||
end
|
||||
]]--
|
||||
|
||||
o = s:option(DummyValue, "type", translate("Type"))
|
||||
function o.cfgvalue(self, section)
|
||||
@ -248,10 +284,14 @@ o = s:option(DummyValue, "server_port", translate("Socket Connected"))
|
||||
o.template = "shadowsocksr/socket"
|
||||
o.width = "10%"
|
||||
o.render = function(self, section, scope)
|
||||
self.transport = s:cfgvalue(section).transport
|
||||
local cfg = s:cfgvalue(section) or {}
|
||||
self.transport = cfg.transport
|
||||
self.type = cfg.type
|
||||
self.v2ray_protocol = cfg.v2ray_protocol
|
||||
if self.transport == 'ws' then
|
||||
self.ws_path = s:cfgvalue(section).ws_path
|
||||
self.tls = s:cfgvalue(section).tls
|
||||
self.ws_path = cfg.ws_path
|
||||
self.tls = cfg.tls
|
||||
self.tls_host = cfg.tls_host
|
||||
end
|
||||
DummyValue.render(self, section, scope)
|
||||
end
|
||||
@ -276,7 +316,8 @@ node.write = function(self, section)
|
||||
uci:set("shadowsocksr", '@global[0]', 'global_server', section)
|
||||
uci:save("shadowsocksr")
|
||||
uci:commit("shadowsocksr")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "restart"))
|
||||
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
|
||||
luci.http.redirect(url("restart"))
|
||||
end
|
||||
|
||||
o = s:option(Flag, "switch_enable", translate("Auto Switch"))
|
||||
@ -285,6 +326,6 @@ function o.cfgvalue(...)
|
||||
return Value.cfgvalue(...) or 1
|
||||
end
|
||||
|
||||
m:append(Template("shadowsocksr/server_list"))
|
||||
m:append(cbi.Template("shadowsocksr/server_list"))
|
||||
|
||||
return m
|
||||
|
||||
@ -5,53 +5,281 @@
|
||||
<%
|
||||
require "luci.sys"
|
||||
function is_js_luci()
|
||||
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
|
||||
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
|
||||
end
|
||||
-%>
|
||||
|
||||
<script src="<%=resource%>/view/shadowsocksr/Sortable.min.js"></script>
|
||||
|
||||
<style>
|
||||
table th, .table .th {
|
||||
text-align: center;
|
||||
background-color: #eeeeee !important;
|
||||
color: #333333 !important;
|
||||
/*line-height: 3 !important;*/
|
||||
padding: 8px 4px !important;
|
||||
border: 1px solid #ddd !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/*.cbi-section-table th:last-child {
|
||||
display: none;
|
||||
}*/
|
||||
|
||||
table td, .table .td {
|
||||
text-align: center;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
border: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.cbi-section-table td:nth-child(2),
|
||||
.cbi-section-table th:nth-child(2) {
|
||||
width: 180px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.node-wrapper {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: center !important;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.node-wrapper .cbi-button,
|
||||
.node-wrapper input[type="button"],
|
||||
.node-wrapper input[type="submit"] {
|
||||
margin: 0 !important;
|
||||
padding: 0 4px !important;
|
||||
height: 32px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
line-height: 1 !important;
|
||||
font-size: 17px !important;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap !important;
|
||||
border: 1px solid #ccc !important;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.node-wrapper .drag-handle {
|
||||
flex: 0 0 32px !important;
|
||||
height: 32px !important;
|
||||
cursor: grab !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
user-select: none;
|
||||
order: 100;
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.node-wrapper .drag-handle:active {
|
||||
cursor: grabbing !important;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.node-wrapper .drag-handle svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #777;
|
||||
transition: fill 0.2s;
|
||||
}
|
||||
|
||||
.node-wrapper .drag-handle:hover svg {
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
/* 测试按钮样式 */
|
||||
/*.socket-connected {
|
||||
display: flex;
|
||||
flex-direction: column; /* 垂直排列 */
|
||||
align-items: center; /* 水平居中 */
|
||||
gap: 4px; /* 按钮与文本间距 */
|
||||
}*/
|
||||
|
||||
.socket-connected {
|
||||
flex: 0 0 40px;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1152px) {
|
||||
td[data-title]::after {
|
||||
content: none !important;
|
||||
display: none !important;
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.cbi-section-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cbi-section-table tbody tr {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.cbi-section-table tbody tr td {
|
||||
display: inline-block;
|
||||
width: auto !important;
|
||||
border: none !important;
|
||||
padding: 4px 6px !important;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cbi-section-table td[data-title]::before {
|
||||
content: attr(data-title) ": ";
|
||||
font-weight: bold;
|
||||
margin-right: 4px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.cbi-section-table td.cbi-section-actions {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #eee !important;
|
||||
padding-top: 8px !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.node-wrapper {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.node-wrapper .cbi-button {
|
||||
flex: 0 1 auto;
|
||||
min-width: 60px;
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* 拖拽手柄在小屏上可隐藏(节省空间) */
|
||||
/*.node-wrapper .drag-handle {
|
||||
display: none !important;
|
||||
}*/
|
||||
}
|
||||
|
||||
.dragging-row {
|
||||
background-color: rgba(131, 191, 255, 0.4) !important;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#save_order_tip {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 30%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
padding: 15px 30px;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
._now_use_bg {
|
||||
background: #5e72e445 !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
._now_use_bg {
|
||||
background: #4a90e2 !important;
|
||||
}
|
||||
.node-wrapper .drag-handle {
|
||||
background: #333;
|
||||
border-color: #555;
|
||||
}
|
||||
.node-wrapper .drag-handle svg {
|
||||
fill: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏 CBI 自带的添加按钮 */
|
||||
.cbi-section-actions .cbi-button-add,
|
||||
.cbi-section-create .cbi-button-add {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 自定义添加按钮样式(与原按钮一致) */
|
||||
.cbi-section-create .cbi-button-add-custom {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
</style>
|
||||
|
||||
<% if is_js_luci() then -%>
|
||||
<script type="text/javascript">
|
||||
var cbi_t = [];
|
||||
function cbi_t_add(section, tab) {
|
||||
var t = document.getElementById('tab.' + section + '.' + tab);
|
||||
var c = document.getElementById('container.' + section + '.' + tab);
|
||||
|
||||
if( t && c ) {
|
||||
cbi_t[section] = (cbi_t[section] || [ ]);
|
||||
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
|
||||
if (t && c) {
|
||||
cbi_t[section] = (cbi_t[section] || []);
|
||||
cbi_t[section][tab] = { tab: t, container: c, cid: c.id };
|
||||
}
|
||||
}
|
||||
|
||||
function cbi_t_switch(section, tab) {
|
||||
if( cbi_t[section] && cbi_t[section][tab] ) {
|
||||
//在切换选项卡之前,先取消当前激活选项卡的全选状态
|
||||
dechecked_all_node();
|
||||
if (cbi_t[section] && cbi_t[section][tab]) {
|
||||
if (typeof dechecked_all_node === "function") {
|
||||
dechecked_all_node();
|
||||
}
|
||||
var o = cbi_t[section][tab];
|
||||
var h = document.getElementById('tab.' + section);
|
||||
for( var tid in cbi_t[section] ) {
|
||||
for (var tid in cbi_t[section]) {
|
||||
var o2 = cbi_t[section][tid];
|
||||
if( o.tab.id != o2.tab.id ) {
|
||||
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
|
||||
if (o.tab.id != o2.tab.id) {
|
||||
o2.tab.className = o2.tab.className.replace(/cbi-tab/g, "cbi-tab-disabled");
|
||||
o2.container.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
if(h) h.value = tab;
|
||||
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
|
||||
} else {
|
||||
if (h) h.value = tab;
|
||||
o2.tab.className = o2.tab.className.replace(/cbi-tab-disabled/g, "cbi-tab");
|
||||
o2.container.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
<%- else %>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
if (typeof(cbi_t_switch) === "function") {
|
||||
var old_switch = cbi_t_switch;
|
||||
cbi_t_switch = function(section, tab) {
|
||||
dechecked_all_node();
|
||||
return old_switch(section, tab);
|
||||
};
|
||||
(function(){
|
||||
if (typeof(cbi_t_switch)==="function") {
|
||||
var old_switch=cbi_t_switch;
|
||||
cbi_t_switch=function(section,tab){
|
||||
if (typeof dechecked_all_node === "function") {
|
||||
dechecked_all_node();
|
||||
}
|
||||
return old_switch(section,tab);
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@ -59,250 +287,444 @@ end
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
window.addEventListener('load', function () {
|
||||
const doms = document.getElementsByClassName('pingtime');
|
||||
const ports = document.getElementsByClassName("socket-connected");
|
||||
const transports = document.getElementsByClassName("transport");
|
||||
const wsPaths = document.getElementsByClassName("wsPath");
|
||||
const tlss = document.getElementsByClassName("tls");
|
||||
const xhr = (index) => {
|
||||
return new Promise((res) => {
|
||||
const dom = doms[index];
|
||||
const port = ports[index];
|
||||
const transport = transports[index];
|
||||
const wsPath = wsPaths[index];
|
||||
const tls = tlss[index];
|
||||
if (!dom) res();
|
||||
port.innerHTML = '<font style=\"color:#0072c3\">connect</font>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/ping")%>', {
|
||||
index,
|
||||
domain: dom.getAttribute("hint"),
|
||||
port: port.getAttribute("hint"),
|
||||
transport: transport.getAttribute("hint"),
|
||||
wsPath: wsPath.getAttribute("hint"),
|
||||
tls: tls.getAttribute("hint")
|
||||
},
|
||||
(x, result) => {
|
||||
let col = '#ff0000';
|
||||
if (result.ping) {
|
||||
if (result.ping < 300) col = '#ff3300';
|
||||
if (result.ping < 200) col = '#ff7700';
|
||||
if (result.ping < 100) col = '#249400';
|
||||
}
|
||||
dom.innerHTML = `<font style=\"color:${col}\">${(result.ping ? result.ping : "--") + " ms"}</font>`;
|
||||
if (result.socket) {
|
||||
port.innerHTML = '<font style=\"color:#249400\">ok</font>';
|
||||
} else {
|
||||
port.innerHTML = '<font style=\"color:#ff0000\">fail</font>';
|
||||
}
|
||||
res();
|
||||
});
|
||||
});
|
||||
};
|
||||
let task = -1;
|
||||
const thread = () => {
|
||||
task = task + 1;
|
||||
if (doms[task]) {
|
||||
xhr(task).then(thread);
|
||||
var ajaxList = [];
|
||||
// 存储原始节点顺序
|
||||
var originNodeOrder = [];
|
||||
|
||||
function ajaxAbortAll() {
|
||||
if (!window.ajaxList) return;
|
||||
window.ajaxList.forEach(function(xhr) {
|
||||
try { xhr.abort(); } catch(e) {}
|
||||
});
|
||||
window.ajaxList = [];
|
||||
}
|
||||
|
||||
// 获取当前节点顺序
|
||||
function get_node_order() {
|
||||
var ids = [];
|
||||
var rows = document.querySelectorAll("#cbi-shadowsocksr-servers table tbody tr[id^='cbi-shadowsocksr-']");
|
||||
rows.forEach(function(row) {
|
||||
var sectionName = row.id.replace("cbi-shadowsocksr-", "");
|
||||
if (sectionName) ids.push(sectionName);
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
// 检查顺序是否变化
|
||||
function check_order_change() {
|
||||
var new_order = get_node_order();
|
||||
var btn = document.getElementById("save_order_btn");
|
||||
if (btn) {
|
||||
var is_same = (originNodeOrder.length === new_order.length &&
|
||||
originNodeOrder.every((v, i) => v === new_order[i]));
|
||||
btn.style.display = is_same ? "none" : "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
// 移动函数:向上
|
||||
function moveUp(btn) {
|
||||
var row = btn.closest("tr");
|
||||
var prev = row.previousElementSibling;
|
||||
if (prev && prev.matches("tr[id]")) {
|
||||
row.parentNode.insertBefore(row, prev);
|
||||
check_order_change();
|
||||
}
|
||||
}
|
||||
|
||||
// 移动函数:向下
|
||||
function moveDown(btn) {
|
||||
var row = btn.closest("tr");
|
||||
var next = row.nextElementSibling;
|
||||
if (next && next.matches("tr[id]")) {
|
||||
row.parentNode.insertBefore(row, next.nextSibling);
|
||||
check_order_change();
|
||||
}
|
||||
}
|
||||
|
||||
// 移动函数:置顶
|
||||
function moveTop(btn) {
|
||||
var row = btn.closest("tr");
|
||||
var first = row.parentNode.querySelector("tr[id]");
|
||||
if (first && first !== row) {
|
||||
row.parentNode.insertBefore(row, first);
|
||||
check_order_change();
|
||||
}
|
||||
}
|
||||
|
||||
// 移动函数:置底
|
||||
function moveBottom(btn) {
|
||||
var row = btn.closest("tr");
|
||||
var last = row.parentNode.querySelector("tr[id]:last-child");
|
||||
if (last && last !== row) {
|
||||
row.parentNode.insertBefore(row, last.nextSibling);
|
||||
check_order_change();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加节点
|
||||
function to_add_node(protocol) {
|
||||
ajaxAbortAll();
|
||||
window.location.href = '<%=luci.dispatcher.build_url("admin/services/shadowsocksr/add_node")%>?redirect=1';
|
||||
}
|
||||
|
||||
// 编辑节点
|
||||
function to_edit_node(node_id) {
|
||||
ajaxAbortAll();
|
||||
window.location.href = '<%=luci.dispatcher.build_url("admin/services/shadowsocksr/servers")%>/' + node_id;
|
||||
}
|
||||
|
||||
// 删除节点
|
||||
function del_node(node_id) {
|
||||
ajaxAbortAll();
|
||||
if (confirm('<%:Are you sure to delete this node?%>')) {
|
||||
window.location.href = '<%=luci.dispatcher.build_url("admin/services/shadowsocksr/remove_node")%>?id=' + encodeURIComponent(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前使用的节点(后端API)
|
||||
function get_now_use_node() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
ajaxList.push(xhr);
|
||||
xhr.open("GET", '<%=luci.dispatcher.build_url("admin/services/shadowsocksr/get_now_use_node")%>', true);
|
||||
xhr.timeout = 3000;
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
var index = ajaxList.indexOf(xhr);
|
||||
if (index !== -1) ajaxList.splice(index, 1);
|
||||
if (xhr.status == 200) {
|
||||
try {
|
||||
var result = JSON.parse(xhr.responseText);
|
||||
if (result) {
|
||||
apply_node_highlight(result);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < 20; i++) {
|
||||
thread();
|
||||
xhr.ontimeout = function() {
|
||||
// 超时也需移除
|
||||
var index = ajaxList.indexOf(xhr);
|
||||
if (index !== -1) ajaxList.splice(index, 1);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// 应用节点高亮
|
||||
function apply_node_highlight(result) {
|
||||
if (!result) return;
|
||||
|
||||
// 1. 清除旧痕迹
|
||||
var allRows = document.querySelectorAll('tr[id^="cbi-shadowsocksr-"]');
|
||||
allRows.forEach(function(row) {
|
||||
row.classList.remove('_now_use_bg');
|
||||
row.style.removeProperty('background-color');
|
||||
|
||||
var td = row.querySelector("td:first-child");
|
||||
if (td) {
|
||||
td.style.removeProperty('color');
|
||||
td.style.removeProperty('font-weight');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 精准点亮
|
||||
var types = ["TCP", "UDP", "netflix", "socks5"];
|
||||
types.forEach(function(type) {
|
||||
var node_id = result[type];
|
||||
if (node_id) {
|
||||
var target = document.getElementById("cbi-shadowsocksr-" + node_id);
|
||||
if (target) {
|
||||
target.classList.add('_now_use_bg');
|
||||
target.style.setProperty('background-color', '#5e72e430', 'important');
|
||||
|
||||
var td = target.querySelector("td:first-child");
|
||||
if (td) {
|
||||
td.style.setProperty('color', 'red', 'important');
|
||||
td.style.setProperty('font-weight', 'bold', 'important');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 保存节点顺序
|
||||
function save_node_order() {
|
||||
var ids = get_node_order();
|
||||
var btn = document.getElementById("save_order_btn");
|
||||
if(btn) btn.disabled = true;
|
||||
document.getElementById("save_order_tip").style.display = "block";
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
ajaxList.push(xhr);
|
||||
xhr.open("POST", '<%=luci.dispatcher.build_url("admin/services/shadowsocksr/save_node_order")%>', true);
|
||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
var index = ajaxList.indexOf(xhr);
|
||||
if (index !== -1) ajaxList.splice(index, 1);
|
||||
document.getElementById("save_order_tip").style.display = "none";
|
||||
if(btn) btn.disabled = false;
|
||||
if (xhr.status == 200) {
|
||||
originNodeOrder = get_node_order();
|
||||
if(btn) btn.style.display = "none";
|
||||
get_now_use_node();
|
||||
alert("<%:Saved current page order successfully.%>");
|
||||
} else {
|
||||
alert("<%:Save failed!%>");
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send("ids=" + encodeURIComponent(ids.join(",")));
|
||||
}
|
||||
|
||||
// 初始化拖拽排序
|
||||
function initSortable() {
|
||||
var tableBody = document.querySelector("#cbi-shadowsocksr-servers table tbody");
|
||||
if (!tableBody) return;
|
||||
|
||||
// 保存原始顺序
|
||||
originNodeOrder = get_node_order();
|
||||
|
||||
// 获取按钮模板
|
||||
var templateHtml = document.getElementById('node-actions-template').innerHTML;
|
||||
|
||||
var rows = tableBody.querySelectorAll("tr[id]");
|
||||
rows.forEach(function(row) {
|
||||
var actionsCell = row.querySelector(".cbi-section-actions");
|
||||
if (!actionsCell || actionsCell.querySelector(".node-wrapper")) return;
|
||||
|
||||
// 从行 ID 提取节点 ID
|
||||
var node_id = row.id.replace("cbi-shadowsocksr-", "");
|
||||
|
||||
// 替换模板中的占位符 {{id}}
|
||||
var wrapperHtml = templateHtml.replace(/{{id}}/g, node_id);
|
||||
|
||||
// 清空原内容并插入新按钮组
|
||||
actionsCell.innerHTML = wrapperHtml;
|
||||
});
|
||||
|
||||
tableBody.querySelectorAll("tr[id]").forEach(function(row) {
|
||||
var node_id = row.id.replace("cbi-shadowsocksr-", "");
|
||||
|
||||
// 初始化 Hysteria2 节点操作列按钮
|
||||
var actionsCell = row.querySelector(".cbi-section-actions");
|
||||
if (actionsCell && !actionsCell.querySelector(".node-wrapper")) {
|
||||
actionsCell.innerHTML = templateHtml.replace(/{{id}}/g, node_id);
|
||||
}
|
||||
|
||||
// 对 Hysteria2 节点在“连接测试”列插入 Test 按钮
|
||||
var typeEl = row.querySelector('.hy2type');
|
||||
var protoEl = row.querySelector('.hy2proto');
|
||||
var isHy2 = (typeEl && typeEl.getAttribute("hint") === "hysteria2") ||
|
||||
(protoEl && protoEl.getAttribute("hint") === "hysteria2");
|
||||
|
||||
if (isHy2) {
|
||||
var socketCell = row.querySelector('.socket-connected');
|
||||
// 检查是否已经添加过,避免重复添加
|
||||
if (socketCell && !row.querySelector('.socket-manual')) {
|
||||
// 将 <%=section%> 替换为 JS 变量 node_id
|
||||
var testHtml = '<span class="socket-manual" data-section="' + node_id + '">' +
|
||||
' <input class="btn cbi-button cbi-button-apply manual-test-btn" type="button" ' +
|
||||
' value="<%:Test%>" title="<%:TLS handshake test, latency for reference only%>" ' +
|
||||
' onclick="manualTest(this, \'' + node_id + '\')">' +
|
||||
'</span>';
|
||||
|
||||
// 插入到连接测试列
|
||||
// socketCell.insertAdjacentHTML('afterbegin', testHtml);
|
||||
// 替换 wait
|
||||
socketCell.innerHTML = testHtml;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 Sortable 拖拽
|
||||
if (typeof Sortable !== 'undefined') {
|
||||
Sortable.create(tableBody, {
|
||||
handle: ".drag-handle",
|
||||
draggable: "tr[id]",
|
||||
animation: 150,
|
||||
ghostClass: "dragging-row",
|
||||
onEnd: function(evt) {
|
||||
check_order_change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前使用的节点高亮
|
||||
get_now_use_node();
|
||||
}
|
||||
|
||||
// 获取 ping 对应的颜色
|
||||
function getPingColor(ping) {
|
||||
if (!ping || ping <= 0) return "#ff0000";
|
||||
if (ping < 100) return "#249400";
|
||||
if (ping < 200) return "#ff7700";
|
||||
if (ping < 300) return "#ff3300";
|
||||
return "#ff0000";
|
||||
}
|
||||
|
||||
function manualTest(btn, section) {
|
||||
// console.log('manualTest called with section:', section);
|
||||
// 禁用按钮
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Testing...%>';
|
||||
|
||||
var row = btn.closest('tr');
|
||||
if (!row) return;
|
||||
|
||||
var domain = row.querySelector('.pingtime');
|
||||
var port = row.querySelector('.socket-connected');
|
||||
var type = row.querySelector('.hy2type');
|
||||
var proto = row.querySelector('.hy2proto');
|
||||
|
||||
var params = {
|
||||
index: section,
|
||||
domain: domain ? domain.getAttribute('hint') : '',
|
||||
port: port ? port.getAttribute('hint') : '',
|
||||
type: (type ? type.getAttribute("hint") : "") || (proto ? proto.getAttribute("hint") : "")
|
||||
};
|
||||
|
||||
// 调试:在浏览器 F12 控制台查看发送的数据
|
||||
// console.log("正在测试节点", section, params);
|
||||
|
||||
var xhr = XHR.get('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/ping")%>', params,
|
||||
function(x, result) {
|
||||
// btn.disabled = false;
|
||||
// btn.value = '<%:Test%>';
|
||||
var idx = ajaxList.indexOf(x);
|
||||
if (idx !== -1) ajaxList.splice(idx, 1);
|
||||
if (x && x.status === 200 && result) {
|
||||
if (port) {
|
||||
port.style.display = 'inline';
|
||||
if (result.socket) {
|
||||
port.innerHTML = '<font style="color:#249400">ok</font>';
|
||||
} else {
|
||||
port.innerHTML = '<font style="color:#ff0000">fail</font>';
|
||||
}
|
||||
}
|
||||
// 更新延迟
|
||||
if (domain && result.ping) {
|
||||
var ping = parseInt(result.ping);
|
||||
var col = getPingColor(ping);
|
||||
domain.innerHTML =
|
||||
'<font style="color:' + col + '">' + (ping > 0 ? ping : "--") + ' ms</font>';
|
||||
}
|
||||
// 隐藏按钮
|
||||
btn.style.display = 'none';
|
||||
} else {
|
||||
// 请求失败允许重试
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Test%>';
|
||||
if (port) {
|
||||
port.style.display = 'inline';
|
||||
port.innerHTML = '<font style="color:#ff0000">fail</font>';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
if (xhr) ajaxList.push(xhr);
|
||||
}
|
||||
|
||||
// 页面加载完成初始化
|
||||
window.addEventListener("load", function () {
|
||||
// 批量 Ping 初始化
|
||||
const doms = document.getElementsByClassName("pingtime");
|
||||
const ports = document.getElementsByClassName("socket-connected");
|
||||
|
||||
function xhr(index) {
|
||||
return new Promise(resolve => {
|
||||
// 跳过 hysteria2 节点的自动测试
|
||||
const type = document.getElementsByClassName("hy2type")[index];
|
||||
const proto = document.getElementsByClassName("hy2proto")[index];
|
||||
if (type && type.getAttribute("hint") === "hysteria2" || proto && proto.getAttribute("hint") === "hysteria2") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const dom = doms[index];
|
||||
if (!dom) return resolve();
|
||||
const port = ports[index];
|
||||
|
||||
// 获取各项属性,增加空值保护
|
||||
const transport = document.getElementsByClassName("transport")[index];
|
||||
const wsPath = document.getElementsByClassName("wsPath")[index];
|
||||
const tls = document.getElementsByClassName("tls")[index];
|
||||
const host = document.getElementsByClassName("tlshost")[index];
|
||||
|
||||
const params = {
|
||||
index: index,
|
||||
domain: dom.getAttribute("hint"),
|
||||
port: port ? port.getAttribute("hint") : "",
|
||||
transport: transport ? transport.getAttribute("hint") : "",
|
||||
wsPath: wsPath ? wsPath.getAttribute("hint") : "",
|
||||
tls: tls ? tls.getAttribute("hint") : "",
|
||||
host: host ? host.getAttribute("hint") : "",
|
||||
};
|
||||
|
||||
// 调试:在浏览器 F12 控制台查看发送的数据
|
||||
// console.log("正在测试节点 " + index, params);
|
||||
|
||||
port.innerHTML = '<font style=\"color:#0072c3\"><%:connect%></font>';
|
||||
var xhr = XHR.get('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/ping")%>', params,
|
||||
function (x, result) {
|
||||
var idx = ajaxList.indexOf(x);
|
||||
if (idx !== -1) ajaxList.splice(idx, 1);
|
||||
if (x && x.status === 200 && result) {
|
||||
let ping = result.ping ? parseInt(result.ping) : 0;
|
||||
let col = getPingColor(ping);
|
||||
if (dom) {
|
||||
dom.innerHTML = '<font style="color:' + col + '">' + (ping > 0 ? ping : "--") + ' ms</font>';
|
||||
}
|
||||
if (port) {
|
||||
port.innerHTML = result.socket ?
|
||||
'<font style="color:#249400">ok</font>' :
|
||||
'<font style="color:#ff0000">fail</font>';
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
if (xhr) ajaxList.push(xhr);
|
||||
});
|
||||
}
|
||||
|
||||
// 顺序执行 Ping 测试
|
||||
let task = -1;
|
||||
function worker() {
|
||||
task++;
|
||||
if (doms[task]) xhr(task).then(worker);
|
||||
}
|
||||
for (let i = 0; i < 20; i++) worker();
|
||||
|
||||
// 初始化排序功能
|
||||
initSortable();
|
||||
|
||||
// 绑定保存按钮点击事件
|
||||
var saveBtn = document.getElementById("save_order_btn");
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = save_node_order;
|
||||
}
|
||||
});
|
||||
|
||||
function cbi_row_drop(fromId, toId, store, isToBottom) {
|
||||
var fromNode = document.getElementById(fromId);
|
||||
var toNode = document.getElementById(toId);
|
||||
if (!fromNode || !toNode) return false;
|
||||
|
||||
var table = fromNode.parentNode;
|
||||
while (table && table.nodeName.toLowerCase() != "table")
|
||||
table = table.parentNode;
|
||||
if (!table) return false;
|
||||
|
||||
var ids = [];
|
||||
if (isToBottom) {
|
||||
toNode.parentNode.appendChild(fromNode);
|
||||
} else {
|
||||
fromNode.parentNode.insertBefore(fromNode, toNode);
|
||||
}
|
||||
for (var idx = 1; idx < table.rows.length; idx++) {
|
||||
table.rows[idx].className = table.rows[idx].className.replace(
|
||||
/cbi-rowstyle-[12]/,
|
||||
"cbi-rowstyle-" + (1 + (idx % 2))
|
||||
);
|
||||
|
||||
if (table.rows[idx].id && table.rows[idx].id.match(/-([^\-]+)$/))
|
||||
ids.push(RegExp.$1);
|
||||
}
|
||||
var input = document.getElementById(store);
|
||||
if (input) input.value = ids.join(" ");
|
||||
return false;
|
||||
}
|
||||
|
||||
// set tr draggable
|
||||
function enableDragForTable(table_selector, store) {
|
||||
// 添加 CSS 样式
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
tr[draggable="true"] {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
/* 添加置顶和置底按钮的间距 */
|
||||
.cbi-button-top,
|
||||
.cbi-button-bottom {
|
||||
margin-right: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
var trs = document.querySelectorAll(table_selector + " tr");
|
||||
if (!trs || trs.length < 2) {
|
||||
return;
|
||||
}
|
||||
function ondragstart(ev) {
|
||||
ev.dataTransfer.setData("Text", ev.target.id);
|
||||
}
|
||||
function ondrop(ev) {
|
||||
var from = ev.dataTransfer.getData("Text");
|
||||
cbi_row_drop(from, this.id, store);
|
||||
}
|
||||
function ondragover(ev) {
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
|
||||
// 上移一行
|
||||
function moveUp(id) {
|
||||
// 查找当前行的索引
|
||||
var trList = document.querySelectorAll(table_selector + " tr");
|
||||
var currentIndex = -1;
|
||||
for (var i = 0; i < trList.length; i++) {
|
||||
if (trList[i].id === id) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是第一行数据行,则上移一行
|
||||
if (currentIndex > 1) {
|
||||
var prevRow = trList[currentIndex - 1];
|
||||
cbi_row_drop(id, prevRow.id, store);
|
||||
}
|
||||
}
|
||||
|
||||
// 下移一行
|
||||
function moveDown(id) {
|
||||
// 查找当前行的索引
|
||||
var trList = document.querySelectorAll(table_selector + " tr");
|
||||
var currentIndex = -1;
|
||||
for (var i = 0; i < trList.length; i++) {
|
||||
if (trList[i].id === id) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是最后一行,则下移一行
|
||||
if (currentIndex < trList.length - 1) {
|
||||
var nextRow = trList[currentIndex + 1];
|
||||
// 对于下移,我们需要将当前行插入到下一行的后面
|
||||
var nextNextRow = trList[currentIndex + 2];
|
||||
if (nextNextRow) {
|
||||
cbi_row_drop(id, nextNextRow.id, store, false);
|
||||
} else {
|
||||
// 如果是倒数第二行,移动到最后一行的后面
|
||||
cbi_row_drop(id, nextRow.id, store, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 置顶
|
||||
function moveToTop(id) {
|
||||
var top = document.querySelectorAll(table_selector + " tr")[1];
|
||||
cbi_row_drop(id, top.id, store);
|
||||
}
|
||||
|
||||
// 置底
|
||||
function moveToBottom(id) {
|
||||
//console.log('moveToBottom:', id);
|
||||
var trList = document.querySelectorAll(table_selector + " tr");
|
||||
var bottom = trList[trList.length - 1];
|
||||
cbi_row_drop(id, bottom.id, store, true);
|
||||
}
|
||||
|
||||
for (let index = 1; index < trs.length; index++) {
|
||||
const el = trs[index];
|
||||
el.setAttribute("draggable", true);
|
||||
el.ondragstart = ondragstart;
|
||||
el.ondrop = ondrop;
|
||||
el.ondragover = ondragover;
|
||||
|
||||
// 上移和下移按钮
|
||||
var upBtns = el.querySelectorAll(".cbi-button.cbi-button-up");
|
||||
if (upBtns && upBtns.length > 0) {
|
||||
upBtns.forEach(function (_el) {
|
||||
_el.onclick = function () {
|
||||
moveUp(el.id);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
var downBtns = el.querySelectorAll(".cbi-button.cbi-button-down");
|
||||
if (downBtns && downBtns.length > 0) {
|
||||
downBtns.forEach(function (_el) {
|
||||
_el.onclick = function () {
|
||||
moveDown(el.id);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 添加置顶和置底按钮
|
||||
var actionsCell = el.querySelector(".cbi-section-actions");
|
||||
if (actionsCell) {
|
||||
// 获取现有的按钮容器内的div
|
||||
var buttonDiv = actionsCell.querySelector("div");
|
||||
|
||||
// 如果按钮容器内没有div,则创建一个
|
||||
if (!buttonDiv) {
|
||||
buttonDiv = document.createElement("div");
|
||||
actionsCell.appendChild(buttonDiv);
|
||||
}
|
||||
|
||||
// 置顶按钮
|
||||
var toTopBtn = document.createElement("input");
|
||||
toTopBtn.className = "btn cbi-button cbi-button-top";
|
||||
toTopBtn.type = "button";
|
||||
toTopBtn.value = "<%:To Top%>";
|
||||
toTopBtn.title = "<%:To Top%>";
|
||||
toTopBtn.onclick = function () {
|
||||
moveToTop(el.id);
|
||||
};
|
||||
|
||||
// 置底按钮
|
||||
var toBottomBtn = document.createElement("input");
|
||||
toBottomBtn.className = "btn cbi-button cbi-button-bottom";
|
||||
toBottomBtn.type = "button";
|
||||
toBottomBtn.value = "<%:To Bottom%>";
|
||||
toBottomBtn.title = "<%:To Bottom%>";
|
||||
toBottomBtn.onclick = function () {
|
||||
moveToBottom(el.id);
|
||||
};
|
||||
|
||||
// 将置顶和置底按钮插入到单元格的开头
|
||||
buttonDiv.insertBefore(toBottomBtn, buttonDiv.firstChild);
|
||||
buttonDiv.insertBefore(toTopBtn, buttonDiv.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enable
|
||||
enableDragForTable(
|
||||
"#cbi-shadowsocksr-servers table",
|
||||
"cbi.sts.shadowsocksr.servers"
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<!-- 按钮模板:包含置顶、上移、下移、置底、编辑、删除、拖拽手柄 -->
|
||||
<script type="text/template" id="node-actions-template">
|
||||
<div class="node-wrapper">
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="moveTop(this)" title="<%:To Top%>"/>
|
||||
<input class="btn cbi-button cbi-button-up" type="button" value="<%:Move up%>" onclick="moveUp(this)" title="<%:Move up%>"/>
|
||||
<input class="btn cbi-button cbi-button-down" type="button" value="<%:Move down%>" onclick="moveDown(this)" title="<%:Move down%>"/>
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Bottom%>" onclick="moveBottom(this)" title="<%:To Bottom%>"/>
|
||||
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="to_edit_node('{{id}}')"/>
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="del_node('{{id}}')"/>
|
||||
<span class="drag-handle" title="<%:Drag to reorder%>">
|
||||
<svg viewBox="0 0 320 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M96 112c0-26.5-21.5-48-48-48S0 85.5 0 112s21.5 48 48 48s48-21.5 48-48zm0 144c0-26.5-21.5-48-48-48S0 229.5 0 256s21.5 48 48 48s48-21.5 48-48zm0 144c0-26.5-21.5-48-48-48S0 373.5 0 400s21.5 48 48 48s48-21.5 48-48zM320 112c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zm0 144c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zm0 144c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<div id="save_order_tip"><%:Saving the new order...%></div>
|
||||
<div class="cbi-section-create" style="margin-top: 10px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<input class="btn cbi-button cbi-button-add-custom" type="button" value="<%:Add%>" onclick="to_add_node()">
|
||||
<input class="btn cbi-button cbi-button-apply cbi-button-positive" style="display:none" type="button" id="save_order_btn" value="<%:Save Order%>">
|
||||
</div>
|
||||
|
||||
@ -3,4 +3,7 @@
|
||||
<span class="transport" hint="<%=self.transport%>"></span>
|
||||
<span class="wsPath" hint="<%=self.ws_path%>"></span>
|
||||
<span class="tls" hint="<%=self.tls%>"></span>
|
||||
<span class="tlshost" hint="<%=self.tls_host%>"></span>
|
||||
<span class="hy2type" hint="<%=self.type%>"></span>
|
||||
<span class="hy2proto" hint="<%=self.v2ray_protocol%>"></span>
|
||||
<%+cbi/valuefooter%>
|
||||
|
||||
@ -116,15 +116,21 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
|
||||
if (params.get("protocol")) {
|
||||
if (params.get("protocol") && params.get("protocol").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].checked = true; // 设置 flag_transport 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].dispatchEvent(event); // 触发事件
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.transport_protocol')[0].value = params.get("protocol") || "udp";
|
||||
}
|
||||
if (params.get("pinSHA256")) {
|
||||
if (params.get("pinSHA256") && params.get("pinSHA256").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.pinsha256')[0].value = params.get("pinSHA256") || "";
|
||||
}
|
||||
if (params.get("fm") && params.get("fm").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].checked = true; // 设置 enable_finalmask 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].dispatchEvent(event); // 触发事件
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.finalmask')[0].value = params.get("fm") || "";
|
||||
}
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
|
||||
} else {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
|
||||
@ -163,17 +169,17 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
|| (xray_hy2_type !== "hysteria2" && (params.get("pcs") || params.get("vcn")))) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
|
||||
if (params.get("sni")) {
|
||||
if (params.get("sni") && params.get("protocol").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
|
||||
}
|
||||
if (params.get("alpn")) {
|
||||
if (params.get("alpn") && params.get("alpn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
|
||||
}
|
||||
if (xray_hy2_type !== "hysteria2") {
|
||||
if (params.get("pcs")) {
|
||||
if (params.get("pcs") && params.get("pcs").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertSha')[0].value = params.get("pcs") || "";
|
||||
}
|
||||
if (params.get("vcn")) {
|
||||
if (params.get("vcn") && params.get("vcn").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_CertByName')[0].value = params.get("vcn") || "";
|
||||
}
|
||||
}
|
||||
@ -212,7 +218,7 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
|
||||
var params = Object.fromEntries(new URLSearchParams(query));
|
||||
|
||||
if ( ! params.type) {
|
||||
if ( ss_type !== "v2ray") {
|
||||
// 普通 SS 导入逻辑
|
||||
// 判断是否 SIP002 格式(即含 @)
|
||||
if (url0.indexOf("@") !== -1) {
|
||||
@ -307,12 +313,12 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
}
|
||||
|
||||
// === 填充配置项 ===
|
||||
var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
|
||||
//var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
|
||||
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
|
||||
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
|
||||
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = port;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = password || "";
|
||||
@ -331,6 +337,11 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = false;
|
||||
}
|
||||
|
||||
//if (params.udp === "1") {
|
||||
// document.getElementsByName('cbid.shadowsocksr.' + sid + '.uot')[0].checked = true; // 设置 uot 为 true
|
||||
// document.getElementsByName('cbid.shadowsocksr.' + sid + '.uot')[0].dispatchEvent(event); // 触发事件
|
||||
//}
|
||||
|
||||
if (params.tfo === "1") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].checked = true; // 设置 fast_open 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].dispatchEvent(event); // 触发事件
|
||||
@ -394,6 +405,20 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
);
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.transport', event);
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.encrypt_method_ss', method || params.encryption || "none");
|
||||
if (params.udp === "1") {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.uot', true); // 设置 uot 为 true
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.uot', event); // 触发事件
|
||||
}
|
||||
if (params.fm && params.fm.trim() !== "") {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', true); // 设置 enable_finalmask 为 true
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
|
||||
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.finalmask', params.fm || "");
|
||||
}
|
||||
if (params.tfo === "1") {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.fast_open', true); // 设置 fast_open 为 true
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.fast_open', event); // 触发事件
|
||||
}
|
||||
if ([ "tls", "xtls", "reality" ].includes(params.security)) {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.' + params.security, true);
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.' + params.security, event);
|
||||
@ -578,7 +603,12 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
if (params.get("ech") && params.get("ech").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_ech')[0].checked = true; // 设置 enable_ech 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_ech')[0].dispatchEvent(event); // 触发事件
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.ech_config')[0].value = params.get("ech");
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.ech_config')[0].value = params.get("ech") || "";
|
||||
}
|
||||
if (params.get("fm") && params.get("fm").trim() !== "") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].checked = true; // 设置 enable_finalmask 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].dispatchEvent(event); // 触发事件
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.finalmask')[0].value = params.get("fm") || "";
|
||||
}
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.transport')[0].value =
|
||||
params.get("type") == "http" ? "h2" :
|
||||
@ -725,7 +755,15 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.kcp_domain')[0].value = ssm.host || "";
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.seed')[0].value = ssm.path || "";
|
||||
}
|
||||
if (ssm.tls == "tls") {
|
||||
if (ssm.fm !== "" && ssm.fm !== undefined) {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].checked = true; // 设置 enable_finalmask 为 true
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_finalmask')[0].dispatchEvent(event); // 触发事件
|
||||
var fmElem = document.getElementsByName('cbid.shadowsocksr.' + sid + '.finalmask')[0];
|
||||
if (fmElem) {
|
||||
fmElem.value = decodeURI(ssm.fm);
|
||||
}
|
||||
}
|
||||
if (ssm.tls == "tls" || ssm.tls == "1") {
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true;
|
||||
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event);
|
||||
if (ssm.fp !== "" && ssm.fp !== undefined) {
|
||||
@ -808,6 +846,11 @@ function import_ssr_url(btn, urlname, sid) {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.vless_encryption', params.get("encryption") || "none");
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.tls_flow', params.get("flow") || "none");
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tls_flow', event);
|
||||
if (params.get("fm") && params.get("fm").trim() !== "") {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', true); // 设置 enable_finalmask 为 true
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.finalmask', params.get("fm") || "");
|
||||
}
|
||||
if ([ "tls", "xtls", "reality" ].includes(params.get("security"))) {
|
||||
setElementValue('cbid.shadowsocksr.' + sid + '.' + params.get("security"), true);
|
||||
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.' + params.get("security"), event);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1478,24 +1478,23 @@ start_xhttp_addr() {
|
||||
local xhttp_addr_file="/etc/ssrplus/xhttp_address.txt"
|
||||
local tmp_file="/tmp/.xhttp_addr.tmp"
|
||||
|
||||
# 收集所有 xhttp_extra 地址,去掉空行并去重排序
|
||||
# 收集所有节点的 download_address 值,去掉空行并去重排序
|
||||
{
|
||||
for sec in "$GLOBAL_SERVER" "$SHUNT_SERVER" "$UDP_RELAY_SERVER"; do
|
||||
local extra
|
||||
extra=$(uci_get_by_name "$sec" xhttp_extra)
|
||||
[ -n "$extra" ] && \
|
||||
echo "$extra" | sed -n 's/.*"address":[[:space:]]*"\([^"]*\)".*/\1/p'
|
||||
local addr
|
||||
addr=$(uci_get_by_name "$sec" download_address)
|
||||
[ -n "$addr" ] && echo "$addr"
|
||||
done
|
||||
} | grep -v '^$' | sort -u > "$tmp_file"
|
||||
|
||||
# 如果没有 xhttp_extra 地址,删除旧文件并退出
|
||||
# 如果没有 download_address 地址,删除旧文件并退出
|
||||
if [ ! -s "$tmp_file" ]; then
|
||||
[ -f "$xhttp_addr_file" ] && rm -f "$xhttp_addr_file"
|
||||
rm -f "$tmp_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 比较 MD5 判断 xhttp_extra 地址是否有变化
|
||||
# 比较 MD5 判断 download_address 地址是否有变化
|
||||
local md5_new md5_old
|
||||
md5_new=$(md5sum "$tmp_file" | awk '{print $1}')
|
||||
if [ -f "$xhttp_addr_file" ]; then
|
||||
@ -1504,7 +1503,7 @@ start_xhttp_addr() {
|
||||
md5_old=""
|
||||
fi
|
||||
|
||||
# MD5 不同更新 xhttp_extra 地址文件
|
||||
# MD5 不同更新 download_address 地址文件
|
||||
if [ "$md5_new" != "$md5_old" ]; then
|
||||
mv -f "$tmp_file" "$xhttp_addr_file"
|
||||
logger -t ssrplus-xhttp "xhttp_address.txt updated"
|
||||
|
||||
@ -22,6 +22,35 @@ local xray_version_val = 0
|
||||
|
||||
local node_id = server_section
|
||||
local remarks = server.alias or ""
|
||||
local b64decode = nixio.bin.b64decode
|
||||
|
||||
local function base64Decode(text)
|
||||
local raw = text
|
||||
if not text or text == "" then
|
||||
return ''
|
||||
end
|
||||
text = text:gsub("%z", "")
|
||||
text = text:gsub("_", "/")
|
||||
text = text:gsub("-", "+")
|
||||
local mod4 = #text % 4
|
||||
text = text .. string.sub('====', mod4 + 1)
|
||||
local result = b64decode(text)
|
||||
if result then
|
||||
return result:gsub("%z", "")
|
||||
else
|
||||
return raw
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanEmptyTables(t)
|
||||
if type(t) ~= "table" then return nil end
|
||||
for k, v in pairs(t) do
|
||||
if type(v) == "table" then
|
||||
t[k] = cleanEmptyTables(v)
|
||||
end
|
||||
end
|
||||
return next(t) and t or nil
|
||||
end
|
||||
|
||||
-- 确保正确判断程序是否存在
|
||||
local function is_finded(e)
|
||||
@ -103,6 +132,25 @@ function socks_http()
|
||||
}
|
||||
end
|
||||
function wireguard()
|
||||
-- 处理 reserved 字段,支持逗号分隔的数字或 Base64 编码
|
||||
local reserved = nil
|
||||
if server.reserved then
|
||||
local bytes = {}
|
||||
if not server.reserved:match("[^%d,]+") then
|
||||
-- 纯数字和逗号,解析为数字列表
|
||||
server.reserved:gsub("%d+", function(b)
|
||||
bytes[#bytes + 1] = tonumber(b)
|
||||
end)
|
||||
else
|
||||
-- Base64 编码的二进制数据
|
||||
local result = base64Decode(server.reserved)
|
||||
for i = 1, #result do
|
||||
bytes[i] = result:byte(i)
|
||||
end
|
||||
end
|
||||
reserved = #bytes > 0 and bytes or nil
|
||||
end
|
||||
|
||||
outbound_settings = {
|
||||
secretKey = server.private_key,
|
||||
address = server.local_addresses,
|
||||
@ -116,7 +164,7 @@ function wireguard()
|
||||
}
|
||||
},
|
||||
noKernelTun = (server.kernelmode == "1") and true or false,
|
||||
reserved = server.reserved and { server.reserved } or nil,
|
||||
reserved = reserved,
|
||||
mtu = tonumber(server.mtu)
|
||||
}
|
||||
end
|
||||
@ -305,14 +353,22 @@ end
|
||||
mldsa65Verify = (server.enable_mldsa65verify == '1') and server.reality_mldsa65verify or nil,
|
||||
serverName = server.tls_host
|
||||
} or nil,
|
||||
rawSettings = (server.transport == "raw" or server.transport == "tcp") and {
|
||||
rawSettings = ((server.transport == "raw" or server.transport == "tcp") and (server.tcp_guise and server.tcp_guise ~= "none")) and {
|
||||
-- tcp
|
||||
header = {
|
||||
type = server.tcp_guise,
|
||||
request = (server.tcp_guise == "http") and {
|
||||
-- request
|
||||
path = {server.http_path} or {"/"},
|
||||
headers = {Host = {server.http_host} or {}}
|
||||
path = server.http_path and (function()
|
||||
local t, r = server.http_path, {}
|
||||
for _, v in ipairs(t) do
|
||||
r[#r + 1] = (v == "" and "/" or v)
|
||||
end
|
||||
return r
|
||||
end)() or {"/"},
|
||||
headers = (server.http_path or server.user_agent) and {
|
||||
Host = server.http_path,
|
||||
["User-Agent"] = server.user_agent and {server.user_agent} or nil
|
||||
} or nil
|
||||
} or nil
|
||||
}
|
||||
} or nil,
|
||||
@ -330,34 +386,45 @@ end
|
||||
-- ws
|
||||
host = server.ws_host or server.tls_host or nil,
|
||||
path = server.ws_path or "/",
|
||||
headers = server.user_agent and {
|
||||
["User-Agent"] = server.user_agent
|
||||
} or nil,
|
||||
maxEarlyData = tonumber(server.ws_ed) or nil,
|
||||
earlyDataHeaderName = server.ws_ed_header or nil
|
||||
earlyDataHeaderName = server.ws_ed_header or nil,
|
||||
heartbeatPeriod = tonumber(server.ws_heartbeatPeriod) or nil
|
||||
} or nil,
|
||||
httpupgradeSettings = (server.transport == "httpupgrade") and {
|
||||
-- httpupgrade
|
||||
host = (server.httpupgrade_host or server.tls_host) or nil,
|
||||
path = server.httpupgrade_path or ""
|
||||
path = server.httpupgrade_path or "",
|
||||
headers = server.user_agent and {
|
||||
["User-Agent"] = server.user_agent
|
||||
} or nil
|
||||
} or nil,
|
||||
xhttpSettings = (server.transport == "xhttp" or server.transport == "splithttp") and {
|
||||
-- xhttp
|
||||
mode = server.xhttp_mode or "auto",
|
||||
host = (server.xhttp_host or server.tls_host) or nil,
|
||||
path = server.xhttp_path or "/",
|
||||
extra = (server.enable_xhttp_extra == "1" and server.xhttp_extra) and (function()
|
||||
local success, parsed = pcall(json.parse, server.xhttp_extra)
|
||||
if not success or not parsed then return nil end
|
||||
-- 如果包含 "extra" 节,就使用它,否则直接使用 tbl
|
||||
local tbl = parsed.extra or parsed
|
||||
-- 枚举第1层字段,如果值为空表或 nil 就删除(简单容错)
|
||||
for k, v in pairs(tbl) do
|
||||
if type(v) == "table" and next(v) == nil then
|
||||
tbl[k] = nil
|
||||
elseif v == nil then
|
||||
tbl[k] = nil
|
||||
extra = (function()
|
||||
local extra = {}
|
||||
-- 解析 xhttp_extra(Base64 编码的 JSON)
|
||||
if (server.enable_xhttp_extra == "1" and server.xhttp_extra) then
|
||||
local ok, parsed = pcall(json.parse, base64Decode(server.xhttp_extra))
|
||||
if ok and type(parsed) == "table" then
|
||||
extra = parsed.extra or parsed -- 取 "extra" 节,若无则整个 parsed
|
||||
end
|
||||
end
|
||||
return tbl
|
||||
end)() or nil
|
||||
-- 处理 User-Agent
|
||||
if server.user_agent and server.user_agent ~= "" then
|
||||
extra.headers = extra.headers or {}
|
||||
if not extra.headers["User-Agent"] and not extra.headers["user-agent"] then
|
||||
extra.headers["User-Agent"] = server.user_agent
|
||||
end
|
||||
end
|
||||
-- 递归清理空表(如空 headers 会被删除)
|
||||
return cleanEmptyTables(extra)
|
||||
end)()
|
||||
} or nil,
|
||||
httpSettings = (server.transport == "h2") and {
|
||||
-- h2
|
||||
@ -374,12 +441,13 @@ end
|
||||
} or nil,
|
||||
grpcSettings = (server.transport == "grpc") and {
|
||||
-- grpc
|
||||
serviceName = server.serviceName or "",
|
||||
multiMode = (server.grpc_mode == "multi") and true or false,
|
||||
idle_timeout = tonumber(server.idle_timeout) or nil,
|
||||
health_check_timeout = tonumber(server.health_check_timeout) or nil,
|
||||
serviceName = (server.serviceName and server.serviceName ~= "") and server.serviceName or nil,
|
||||
multiMode = (server.grpc_mode == "multi") and true or nil,
|
||||
idle_timeout = server.idle_timeout and (tonumber(server.idle_timeout) < 10 and 10 or tonumber(server.idle_timeout)) or nil,
|
||||
health_check_timeout = server.health_check_timeout and tonumber(server.health_check_timeout) or nil,
|
||||
permit_without_stream = (server.permit_without_stream == "1") and true or nil,
|
||||
initial_windows_size = tonumber(server.initial_windows_size) or nil
|
||||
initial_windows_size = server.initial_windows_size and tonumber(server.initial_windows_size) or nil,
|
||||
user_agent = server.user_agent
|
||||
} or nil,
|
||||
hysteriaSettings = (server.v2ray_protocol == "hysteria2") and {
|
||||
-- hysteria2
|
||||
@ -409,35 +477,43 @@ end
|
||||
keepAlivePeriod = (server.flag_quicparam == "1" and server.keepaliveperiod) and tonumber(server.keepaliveperiod) or nil,
|
||||
disablePathMTUDiscovery = (server.flag_quicparam == "1" and tostring(server.disablepathmtudiscovery) == "1") and true or nil
|
||||
} or nil,
|
||||
finalmask = (server.transport == "kcp") and {
|
||||
udp = (function()
|
||||
local t = {}
|
||||
finalmask = (function()
|
||||
local finalmask
|
||||
if server.transport == "kcp" then
|
||||
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
|
||||
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
|
||||
local udp = {}
|
||||
if server.kcp_guise and server.kcp_guise ~= "none" then
|
||||
local g = { type = map[server.kcp_guise] }
|
||||
if server.kcp_guise == "dns" and server.kcp_domain and server.kcp_domain ~= "" then
|
||||
g.settings = { domain = server.kcp_domain }
|
||||
end
|
||||
t[#t + 1] = g
|
||||
udp[#udp+1] = g
|
||||
end
|
||||
local c = { type = (server.seed and server.seed ~= "") and "mkcp-aes128gcm" or "mkcp-original" }
|
||||
if server.seed and server.seed ~= "" then
|
||||
c.settings = { password = server.seed }
|
||||
end
|
||||
t[#t + 1] = c
|
||||
return t
|
||||
end)()
|
||||
} or (server.flag_obfs == "1" and (server.v2ray_protocol == "hysteria2" and server.obfs_type and server.obfs_type ~= "")) and {
|
||||
udp = {
|
||||
{
|
||||
type = server.obfs_type,
|
||||
settings = server.salamander and {
|
||||
password = server.salamander
|
||||
} or nil
|
||||
udp[#udp+1] = c
|
||||
finalmask = { udp = udp }
|
||||
elseif (server.flag_obfs == "1" and (server.v2ray_protocol == "hysteria2" and server.obfs_type and server.obfs_type ~= "")) then
|
||||
finalmask = {
|
||||
udp = {{
|
||||
type = server.obfs_type,
|
||||
settings = server.salamander and {
|
||||
password = server.salamander
|
||||
} or nil
|
||||
}}
|
||||
}
|
||||
}
|
||||
} or nil,
|
||||
end
|
||||
if server.finalmask and server.finalmask ~= "" then
|
||||
local ok, fm = pcall(json.parse, base64Decode(server.finalmask))
|
||||
if ok and type(fm) == "table" then
|
||||
finalmask = fm
|
||||
end
|
||||
end
|
||||
return cleanEmptyTables(finalmask)
|
||||
end)(),
|
||||
sockopt = {
|
||||
mark = 250,
|
||||
tcpFastOpen = (function()
|
||||
@ -474,17 +550,15 @@ if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.e
|
||||
settings = {
|
||||
domainStrategy = (xray_fragment.noise == "1" and xray_noise.enabled == "1") and xray_noise.domainStrategy,
|
||||
fragment = (xray_fragment.fragment == "1") and {
|
||||
packets = (xray_fragment.fragment_packets ~= "") and xray_fragment.fragment_packets or nil,
|
||||
length = (xray_fragment.fragment_length ~= "") and xray_fragment.fragment_length or nil,
|
||||
interval = (xray_fragment.fragment_interval ~= "") and xray_fragment.fragment_interval or nil,
|
||||
maxSplit = (xray_fragment.fragment_maxsplit ~= "") and xray_fragment.fragment_maxsplit or nil
|
||||
packets = (xray_fragment.fragment_packets and xray_fragment.fragment_packets ~= "") and xray_fragment.fragment_packets or nil,
|
||||
length = (xray_fragment.fragment_length and xray_fragment.fragment_length ~= "") and xray_fragment.fragment_length or nil,
|
||||
interval = (xray_fragment.fragment_interval and xray_fragment.fragment_interval ~= "") and xray_fragment.fragment_interval or nil
|
||||
} or nil,
|
||||
noises = (xray_fragment.noise == "1" and xray_noise.enabled == "1") and {
|
||||
{
|
||||
type = xray_noise.type,
|
||||
packet = xray_noise.packet,
|
||||
delay = xray_noise.delay:find("-") and xray_noise.delay or tonumber(xray_noise.delay),
|
||||
applyTo = xray_noise.applyto
|
||||
delay = xray_noise.delay:find("-") and xray_noise.delay or tonumber(xray_noise.delay)
|
||||
}
|
||||
} or nil
|
||||
},
|
||||
@ -752,10 +826,10 @@ local tuic = {
|
||||
certificates = server.certificate and { server.certpath } or nil,
|
||||
udp_relay_mode = server.udp_relay_mode,
|
||||
congestion_control = server.congestion_control,
|
||||
heartbeat = server.heartbeat and server.heartbeat .. "s" or nil,
|
||||
timeout = server.timeout and server.timeout .. "s" or nil,
|
||||
gc_interval = server.gc_interval and server.gc_interval .. "s" or nil,
|
||||
gc_lifetime = server.gc_lifetime and server.gc_lifetime .. "s" or nil,
|
||||
heartbeat = server.heartbeat and tonumber(server.heartbeat) .. "s" or nil,
|
||||
timeout = server.timeout and tonumber(server.timeout) .. "s" or nil,
|
||||
gc_interval = server.gc_interval and tonumber(server.gc_interval) .. "s" or nil,
|
||||
gc_lifetime = server.gc_lifetime and tonumber(server.gc_lifetime) .. "s" or nil,
|
||||
alpn = (server.tuic_alpn and server.tuic_alpn ~= "") and (function()
|
||||
local alpn = {}
|
||||
string.gsub(server.tuic_alpn, '[^,]+', function(w)
|
||||
|
||||
175
luci-app-ssr-plus/root/usr/share/shadowsocksr/hy2_test.sh
Executable file
175
luci-app-ssr-plus/root/usr/share/shadowsocksr/hy2_test.sh
Executable file
@ -0,0 +1,175 @@
|
||||
#!/bin/sh
|
||||
# /usr/share/shadowsocksr/test.sh
|
||||
|
||||
CONFIG="shadowsocksr"
|
||||
LOCK_PATH=/tmp/lock
|
||||
TMP_PATH=/var/etc/ssrplus
|
||||
|
||||
uci_get_by_name() {
|
||||
local ret=$(uci -q get $CONFIG.$1.$2 2>/dev/null)
|
||||
echo "${ret:=$3}"
|
||||
}
|
||||
|
||||
uci_get_by_type() {
|
||||
local ret=$(uci -q get $CONFIG.@$1[0].$2 2>/dev/null)
|
||||
echo "${ret:=$3}"
|
||||
}
|
||||
|
||||
check_port_exists() {
|
||||
local port=$1
|
||||
local protocol=$2
|
||||
[ -n "$protocol" ] || protocol="tcp,udp"
|
||||
local result=
|
||||
if [ "$protocol" = "tcp" ]; then
|
||||
result=$(netstat -tln | grep -c ":$port ")
|
||||
elif [ "$protocol" = "udp" ]; then
|
||||
result=$(netstat -uln | grep -c ":$port ")
|
||||
elif [ "$protocol" = "tcp,udp" ]; then
|
||||
result=$(netstat -tuln | grep -c ":$port ")
|
||||
fi
|
||||
echo "${result}"
|
||||
}
|
||||
|
||||
set_cache_var() {
|
||||
local key="${1}"
|
||||
shift 1
|
||||
local val="$@"
|
||||
[ -n "${key}" ] && [ -n "${val}" ] && {
|
||||
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
|
||||
echo "${key}=\"${val}\"" >> $TMP_PATH/var
|
||||
eval ${key}=\"${val}\"
|
||||
}
|
||||
}
|
||||
|
||||
get_cache_var() {
|
||||
local key="${1}"
|
||||
[ -n "${key}" ] && [ -s "$TMP_PATH/var" ] && {
|
||||
echo $(cat $TMP_PATH/var | grep "^${key}=" | awk -F '=' '{print $2}' | tail -n 1 | awk -F'"' '{print $2}')
|
||||
}
|
||||
}
|
||||
|
||||
#uci_get_by_port() {
|
||||
# local port=$1
|
||||
# while netstat -tuln 2>/dev/null | grep -q ":${port} "; do
|
||||
# port=$((port + 1))
|
||||
# done
|
||||
# echo $port
|
||||
|
||||
uci_get_by_port() {
|
||||
local default_start_port=2001
|
||||
local min_port=1025
|
||||
local max_port=49151
|
||||
local port="$1"
|
||||
local protocol=$(echo "$2" | tr 'A-Z' 'a-z')
|
||||
local LOCK_FILE="${LOCK_PATH}/${CONFIG}_get_prot.lock"
|
||||
while ! mkdir "$LOCK_FILE" 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
if [ "$port" = "auto" ]; then
|
||||
local now last_time diff last_port
|
||||
now=$(date +%s 2>/dev/null)
|
||||
last_time=$(get_cache_var "last_get_new_port_time")
|
||||
if [ -n "$now" ] && [ -n "$last_time" ]; then
|
||||
diff=$(expr "$now" - "$last_time")
|
||||
[ "$diff" -lt 0 ] && diff=$(expr 0 - "$diff")
|
||||
else
|
||||
diff=999
|
||||
fi
|
||||
if [ "$diff" -gt 10 ]; then
|
||||
port=$default_start_port
|
||||
else
|
||||
last_port=$(get_cache_var "last_get_new_port_auto")
|
||||
if [ -n "$last_port" ]; then
|
||||
port=$(expr "$last_port" + 1)
|
||||
else
|
||||
port=$default_start_port
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[ "$port" -lt $min_port -o "$port" -gt $max_port ] && port=$default_start_port
|
||||
local start_port="$port"
|
||||
while :; do
|
||||
if [ "$(check_port_exists "$port" "$protocol")" = 0 ]; then
|
||||
break
|
||||
fi
|
||||
port=$(expr "$port" + 1)
|
||||
if [ "$port" -gt $max_port ]; then
|
||||
port=$min_port
|
||||
fi
|
||||
[ "$port" = "$start_port" ] && {
|
||||
rmdir "$LOCK_FILE" 2>/dev/null
|
||||
return 1
|
||||
}
|
||||
done
|
||||
if [ "$1" = "auto" ]; then
|
||||
set_cache_var "last_get_new_port_auto" "$port"
|
||||
[ -n "$now" ] && set_cache_var "last_get_new_port_time" "$now"
|
||||
fi
|
||||
rmdir "$LOCK_FILE" 2>/dev/null
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
url_test_hy2() {
|
||||
local node_id=$1
|
||||
|
||||
# 读取配置
|
||||
local server=$(uci_get_by_name ${node_id} server)
|
||||
local port=$(uci_get_by_name ${node_id} server_port)
|
||||
local auth=$(uci_get_by_name ${node_id} hy2_auth)
|
||||
local tls=$(uci_get_by_name ${node_id} tls)
|
||||
local insecure=$(uci_get_by_name ${node_id} insecure)
|
||||
local tls_host=$(uci_get_by_name ${node_id} tls_host)
|
||||
|
||||
# 获取本地端口
|
||||
# local tmp_port=$(uci_get_by_port 48900 tcp,udp)
|
||||
local tmp_port=$(uci_get_by_port auto tcp,udp)
|
||||
|
||||
# 生成Hysteria2配置文件
|
||||
local config_file="/tmp/hy2_test_${node_id}.yaml"
|
||||
cat > "$config_file" <<-EOF
|
||||
server: ${server}:${port}
|
||||
auth: "${auth}"
|
||||
tls:
|
||||
insecure: true
|
||||
EOF
|
||||
|
||||
# 如果 tls_host 非空,动态添加 sni 行
|
||||
[ -n "$tls_host" ] && echo " sni: \"${tls_host}\"" >> "$config_file"
|
||||
|
||||
# 追加 socks5 监听配置
|
||||
cat >> "$config_file" <<-EOF
|
||||
socks5:
|
||||
listen: 127.0.0.1:${tmp_port}
|
||||
EOF
|
||||
|
||||
# echo "Debug: 配置文件已生成: $config_file" >&2
|
||||
|
||||
# 启动Hysteria2客户端
|
||||
hysteria client --disable-update-check -c "$config_file" >/dev/null 2>&1 &
|
||||
local pid=$!
|
||||
echo $pid > "/tmp/hy2_test_${node_id}.pid"
|
||||
|
||||
# 等待端口启动
|
||||
sleep 1
|
||||
|
||||
# 测试代理
|
||||
# local result=$(curl --connect-timeout 3 --max-time 3 -s -o /dev/null -I -w "%{http_code}:%{time_pretransfer}" --socks5 127.0.0.1:${tmp_port} "${probeUrl}" 2>/dev/null)
|
||||
local curlx="socks5h://127.0.0.1:${tmp_port}"
|
||||
local probeUrl=$(uci_get_by_type server_subscribe url_test_url https://www.google.com/generate_204)
|
||||
local result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}" 2>/dev/null)
|
||||
|
||||
# 清理
|
||||
# kill -9 $pid 2>/dev/null
|
||||
local pid_file="/tmp/hy2_test_${node_id}.pid"
|
||||
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
|
||||
pgrep -af "hysteria.*${config_file}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1
|
||||
rm -f "$config_file" "$pid_file"
|
||||
|
||||
echo $result
|
||||
}
|
||||
|
||||
case $1 in
|
||||
url_test_hy2)
|
||||
url_test_hy2 $2
|
||||
;;
|
||||
esac
|
||||
@ -28,6 +28,7 @@ config server_subscribe
|
||||
option auto_update_day_time '2'
|
||||
option auto_update_min_time '0'
|
||||
option ss_type 'ss-rust'
|
||||
option url_test_url 'https://www.google.com/generate_204'
|
||||
option user_agent 'v2rayN/9.99'
|
||||
option xray_hy2_type 'hysteria2'
|
||||
option filter_words '过期/套餐/剩余/QQ群/官网/防失联/回国'
|
||||
|
||||
@ -16,6 +16,7 @@ local tinsert = table.insert
|
||||
local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub
|
||||
local jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify
|
||||
local b64decode = nixio.bin.b64decode
|
||||
local b64encode = nixio.bin.b64encode
|
||||
local URL = require "url"
|
||||
local cache = {}
|
||||
local nodeResult = setmetatable({}, {__index = cache}) -- update result
|
||||
@ -34,20 +35,34 @@ local ss_type = ucic:get_first(name, 'server_subscribe', 'ss_type', 'ss-rust')
|
||||
-- 根据 ss_type 选择对应的程序
|
||||
local ss_program = "sslocal"
|
||||
if ss_type == "ss-rust" then
|
||||
ss_program = "sslocal" -- Rust 版本使用 sslocal
|
||||
ss_program = "sslocal"
|
||||
elseif ss_type == "ss-libev" then
|
||||
ss_program = "ss-redir" -- Libev 版本使用 ss-redir
|
||||
ss_program = "ss-redir"
|
||||
elseif ss_type == "v2ray" then
|
||||
ss_program = "xray"
|
||||
end
|
||||
-- 从 UCI 配置读取 xray_hy2_type 设置
|
||||
local xray_hy2_type = ucic:get_first(name, 'server_subscribe', 'xray_hy2_type', 'hysteria2')
|
||||
local xray_hy2_program = "hysteria"
|
||||
if xray_hy2_type == "xray" then
|
||||
if xray_hy2_type == "v2ray" then
|
||||
xray_hy2_program = "xray" -- Hysteria2 使用 Xray
|
||||
elseif xray_hy2_type == "hysteria2" then
|
||||
xray_hy2_program = "hysteria" -- Hysteria2 使用 Hysteria
|
||||
end
|
||||
local v2_ss = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= "" and "ss" or "v2ray"
|
||||
local has_ss_type = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= "" and ss_type
|
||||
local v2_ss_exists = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= ""
|
||||
-- 初始化变量
|
||||
local v2_ss = nil
|
||||
local has_v2_ss_type = nil
|
||||
if v2_ss_exists then
|
||||
if ss_type == "v2ray" then
|
||||
-- 使用 Xray
|
||||
v2_ss = "v2ray"
|
||||
has_v2_ss_type = "shadowsocks"
|
||||
else
|
||||
-- 使用 SS (rust 或 libev)
|
||||
v2_ss = "ss"
|
||||
end
|
||||
end
|
||||
local v2_tj = luci.sys.exec('type -t -p trojan') ~= "" and "trojan" or "v2ray"
|
||||
-- 检查程序是否存在
|
||||
local program_exists = luci.sys.exec('type -t -p ' .. xray_hy2_program .. ' 2>/dev/null') ~= ""
|
||||
@ -176,6 +191,21 @@ local function base64Decode(text)
|
||||
return raw
|
||||
end
|
||||
end
|
||||
local function base64Encode(text)
|
||||
if not text or text == "" then
|
||||
return ''
|
||||
end
|
||||
local result = b64encode(text)
|
||||
if result then
|
||||
result = result:gsub("%z", "")
|
||||
result = result:gsub("/", "_")
|
||||
result = result:gsub("+", "-")
|
||||
result = result:gsub("=", "")
|
||||
return result
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
-- 检查数组(table)中是否存在某个字符值
|
||||
-- https://www.04007.cn/article/135.html
|
||||
local function checkTabValue(tab)
|
||||
@ -196,7 +226,7 @@ local function isCompleteJSON(str)
|
||||
return success
|
||||
end
|
||||
-- 处理数据
|
||||
local function processData(szType, content)
|
||||
local function processData(szType, content, cfgid)
|
||||
local result = {type = szType, local_port = 1234, kcp_param = '--nocomp'}
|
||||
-- 检查JSON的格式如不完整丢弃
|
||||
if not (szType == "sip008" or szType == "ssd") then
|
||||
@ -215,17 +245,24 @@ local function processData(szType, content)
|
||||
-- log(k.."="..v)
|
||||
-- end
|
||||
|
||||
-- 如果 hy2 程序未安装则跳过订阅
|
||||
if not (hy2_type or has_xray_hy2_type) then
|
||||
-- 如果 hy2 或 Xray 程序未安装则跳过订阅
|
||||
if not hy2_type then
|
||||
return nil
|
||||
end
|
||||
|
||||
if xray_hy2_type == "hysteria2" then
|
||||
if params.protocol then
|
||||
if params.protocol and params.protocol ~= "" then
|
||||
result.flag_transport = "1"
|
||||
result.transport_protocol = params.protocol or "udp"
|
||||
result.transport_protocol = params.protocol
|
||||
else
|
||||
result.flag_transport = "1"
|
||||
result.transport_protocol = "udp"
|
||||
end
|
||||
if params.pinSHA256 then
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmask = base64Encode(params.fm)
|
||||
end
|
||||
if params.pinSHA256 and params.pinSHA256 ~= "" then
|
||||
result.pinsha256 = params.pinSHA256
|
||||
end
|
||||
else
|
||||
@ -233,7 +270,6 @@ local function processData(szType, content)
|
||||
end
|
||||
|
||||
result.alias = url.fragment and UrlDecode(url.fragment) or nil
|
||||
result.xray_hy2_type = xray_hy2_type
|
||||
result.type = hy2_type
|
||||
result.server = url.host
|
||||
result.server_port = url.port or 443
|
||||
@ -379,14 +415,14 @@ local function processData(szType, content)
|
||||
-- 检查 extra 参数是否存在且非空
|
||||
if info.extra and info.extra ~= "" then
|
||||
result.enable_xhttp_extra = "1"
|
||||
result.xhttp_extra = info.extra
|
||||
result.xhttp_extra = base64Encode(info.extra)
|
||||
end
|
||||
-- 尝试解析 JSON 数据
|
||||
local success, Data = pcall(jsonParse, info.extra or "")
|
||||
if success and type(Data) == "table" then
|
||||
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
|
||||
or (Data.downloadSettings and Data.downloadSettings.address)
|
||||
result.download_address = (address and address ~= "") and address or nil
|
||||
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
|
||||
else
|
||||
-- 如果解析失败,清空下载地址
|
||||
result.download_address = nil
|
||||
@ -430,6 +466,11 @@ local function processData(szType, content)
|
||||
if info.security then
|
||||
result.security = info.security
|
||||
end
|
||||
if info.fm and info.fm ~= "" then
|
||||
info.fm = UrlDecode(info.fm)
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmask = base64Encode(info.fm)
|
||||
end
|
||||
if info.tls == "tls" or info.tls == "1" then
|
||||
result.tls = "1"
|
||||
result.fingerprint = info.fp
|
||||
@ -490,7 +531,12 @@ local function processData(szType, content)
|
||||
end
|
||||
end
|
||||
|
||||
if not params.type or params.type == "" then
|
||||
if params.tfo and params.tfo ~= "" then
|
||||
-- 处理 fast open 参数
|
||||
result.fast_open = params.tfo
|
||||
end
|
||||
|
||||
if v2_ss ~= "v2ray" then
|
||||
local is_old_format = find_index:find("@") and not find_index:find("://.*@")
|
||||
local old_base64, host_port, userinfo, server, port, method, password
|
||||
|
||||
@ -557,23 +603,17 @@ local function processData(szType, content)
|
||||
end
|
||||
|
||||
-- 如果 SS 程序未安装则跳过订阅
|
||||
if not (v2_ss or has_ss_type) then
|
||||
if not v2_ss then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- 填充 result
|
||||
result.type = v2_ss
|
||||
result.has_ss_type = has_ss_type
|
||||
result.encrypt_method_ss = method
|
||||
result.password = password
|
||||
result.server = server
|
||||
result.server_port = port
|
||||
|
||||
if params.tfo then
|
||||
-- 处理 fast open 参数
|
||||
result.fast_open = params.tfo
|
||||
end
|
||||
|
||||
-- 插件处理
|
||||
if params.plugin then
|
||||
local plugin_info = UrlDecode(params.plugin)
|
||||
@ -638,9 +678,13 @@ local function processData(szType, content)
|
||||
local url = URL.parse("http://" .. info)
|
||||
local params = url.query
|
||||
|
||||
v2_ss = "v2ray"
|
||||
-- 如果 Xray 程序未安装则跳过订阅
|
||||
if not v2_ss then
|
||||
return nil
|
||||
end
|
||||
|
||||
result.type = v2_ss
|
||||
result.v2ray_protocol = "shadowsocks"
|
||||
result.v2ray_protocol = has_v2_ss_type
|
||||
result.server = url.host
|
||||
result.server_port = url.port
|
||||
|
||||
@ -655,6 +699,11 @@ local function processData(szType, content)
|
||||
result.encrypt_method_ss = params.encryption or "none"
|
||||
end
|
||||
|
||||
if params.udp then
|
||||
-- 处理 udp 参数
|
||||
result.uot = params.udp
|
||||
end
|
||||
|
||||
result.transport = params.type or "raw"
|
||||
if result.transport == "tcp" then
|
||||
result.transport = "raw"
|
||||
@ -690,6 +739,11 @@ local function processData(szType, content)
|
||||
result.enable_ech = "1"
|
||||
result.ech_config = params.ech
|
||||
end
|
||||
-- 检查 finalmaskg 参数是否存在且非空
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmaskg = base64Encode(params.fm)
|
||||
end
|
||||
-- 检查 pqv 参数是否存在且非空
|
||||
if params.pqv and params.pqv ~= "" then
|
||||
result.enable_mldsa65verify = "1"
|
||||
@ -714,14 +768,14 @@ local function processData(szType, content)
|
||||
-- 检查 extra 参数是否存在且非空
|
||||
if params.extra and params.extra ~= "" then
|
||||
result.enable_xhttp_extra = "1"
|
||||
result.xhttp_extra = params.extra
|
||||
result.xhttp_extra = base64Encode(params.extra)
|
||||
end
|
||||
-- 尝试解析 JSON 数据
|
||||
local success, Data = pcall(jsonParse, params.extra or "")
|
||||
if success and type(Data) == "table" then
|
||||
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
|
||||
or (Data.downloadSettings and Data.downloadSettings.address)
|
||||
result.download_address = address and address ~= "" and address or nil
|
||||
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
|
||||
else
|
||||
-- 如果解析失败,清空下载地址
|
||||
result.download_address = nil
|
||||
@ -760,8 +814,12 @@ local function processData(szType, content)
|
||||
end
|
||||
elseif szType == "sip008" then
|
||||
result.type = v2_ss
|
||||
result.v2ray_protocol = (v2_ss == "v2ray") and "shadowsocks" or nil
|
||||
result.has_ss_type = has_ss_type
|
||||
if v2_ss ~= "v2ray" then
|
||||
result.has_ss_type = has_ss_type
|
||||
else
|
||||
result.xray_has_ss_type = "v2ray"
|
||||
result.v2ray_protocol = has_v2_ss_type
|
||||
end
|
||||
result.server = content.server
|
||||
result.server_port = content.server_port
|
||||
result.password = content.password
|
||||
@ -774,8 +832,12 @@ local function processData(szType, content)
|
||||
end
|
||||
elseif szType == "ssd" then
|
||||
result.type = v2_ss
|
||||
result.v2ray_protocol = (v2_ss == "v2ray") and "shadowsocks" or nil
|
||||
result.has_ss_type = has_ss_type
|
||||
if v2_ss ~= "v2ray" then
|
||||
result.has_ss_type = has_ss_type
|
||||
else
|
||||
result.xray_has_ss_type = "v2ray"
|
||||
result.v2ray_protocol = has_v2_ss_type
|
||||
end
|
||||
result.server = content.server
|
||||
result.server_port = content.port
|
||||
result.password = content.password
|
||||
@ -883,6 +945,11 @@ local function processData(szType, content)
|
||||
result.enable_ech = "1"
|
||||
result.ech_config = params.ech
|
||||
end
|
||||
-- 检查 finalmaskg 参数是否存在且非空
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmaskg = base64Encode(params.fm)
|
||||
end
|
||||
-- 处理传输协议
|
||||
result.transport = params.type or "raw" -- 默认传输协议为 raw
|
||||
if result.transport == "tcp" then
|
||||
@ -910,14 +977,14 @@ local function processData(szType, content)
|
||||
-- 检查 extra 参数是否存在且非空
|
||||
if params.extra and params.extra ~= "" then
|
||||
result.enable_xhttp_extra = "1"
|
||||
result.xhttp_extra = params.extra
|
||||
result.xhttp_extra = base64Encode(params.extra)
|
||||
end
|
||||
-- 尝试解析 JSON 数据
|
||||
local success, Data = pcall(jsonParse, params.extra or "")
|
||||
if success and type(Data) == "table" then
|
||||
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
|
||||
or (Data.downloadSettings and Data.downloadSettings.address)
|
||||
result.download_address = address and address ~= "" and address or nil
|
||||
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
|
||||
else
|
||||
-- 如果解析失败,清空下载地址
|
||||
result.download_address = nil
|
||||
@ -1007,6 +1074,18 @@ local function processData(szType, content)
|
||||
end
|
||||
end
|
||||
|
||||
-- ECH 参数(TLS 才有)
|
||||
if security == "tls" and params.ech and params.ech ~= "" then
|
||||
result.enable_ech = "1"
|
||||
result.ech_config = params.ech
|
||||
end
|
||||
|
||||
-- 处理 finalmask 参数
|
||||
if params.fm and params.fm ~= "" then
|
||||
result.enable_finalmask = "1"
|
||||
result.finalmask = base64Encode(params.fm)
|
||||
end
|
||||
|
||||
-- 处理 pinsha256 参数
|
||||
if params.pcs and params.pcs ~= "" then
|
||||
result.tls_CertSha = params.pcs
|
||||
@ -1023,19 +1102,13 @@ local function processData(szType, content)
|
||||
result.reality_shortid = params.sid
|
||||
result.reality_spiderx = params.spx and UrlDecode(params.spx) or nil
|
||||
|
||||
-- PQ 验证参数
|
||||
-- PQV 验证参数
|
||||
if params.pqv and params.pqv ~= "" then
|
||||
result.enable_mldsa65verify = "1"
|
||||
result.reality_mldsa65verify = params.pqv
|
||||
end
|
||||
end
|
||||
|
||||
-- ECH 参数(TLS 才有)
|
||||
if security == "tls" and params.ech and params.ech ~= "" then
|
||||
result.enable_ech = "1"
|
||||
result.ech_config = params.ech
|
||||
end
|
||||
|
||||
-- 各种传输类型
|
||||
if result.transport == "ws" then
|
||||
result.ws_host = (result.tls ~= "1" and result.reality ~= "1") and (params.host and UrlDecode(params.host)) or nil
|
||||
@ -1055,13 +1128,13 @@ local function processData(szType, content)
|
||||
end
|
||||
if params.extra and params.extra ~= "" then
|
||||
result.enable_xhttp_extra = "1"
|
||||
result.xhttp_extra = params.extra
|
||||
result.xhttp_extra = base64Encode(params.extra)
|
||||
end
|
||||
local success, Data = pcall(jsonParse, params.extra or "")
|
||||
if success and type(Data) == "table" then
|
||||
local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
|
||||
or (Data.downloadSettings and Data.downloadSettings.address)
|
||||
result.download_address = address and address ~= "" and address or nil
|
||||
result.download_address = (address and address ~= "") and address:gsub("^%[", ""):gsub("%]$", "")
|
||||
else
|
||||
result.download_address = nil
|
||||
end
|
||||
@ -1254,20 +1327,27 @@ local function write_new_md5(groupHash, md5)
|
||||
end
|
||||
|
||||
-- curl
|
||||
local function curl(url)
|
||||
-- 清理 URL 中的隐藏字符
|
||||
url = url:gsub("%s+$", ""):gsub("^%s+", ""):gsub("%z", "")
|
||||
|
||||
-- 构建curl命令(确保 user_agent 为空时不添加 -A 参数)
|
||||
local function curl(url, user_agent)
|
||||
-- 清理 URL 中的隐藏字符和前后空白
|
||||
url = url:gsub("%s+$", ""):gsub("^%s+", ""):gsub("%z", ""):gsub("[\r\n]", "")
|
||||
-- 处理 user_agent 参数
|
||||
local ua_opt = ""
|
||||
if user_agent and user_agent ~= "" then
|
||||
-- 转义双引号,防止破坏 -A 参数
|
||||
local safe_ua = user_agent:gsub("[\r\n]", ""):gsub('[\\"`$]', '\\%0') -- 安全转义
|
||||
ua_opt = '-A "' .. safe_ua .. '"'
|
||||
end
|
||||
-- 安全转义 URL:用单引号包裹,并转义内部的单引号
|
||||
local safe_url = "'" .. url:gsub("'", "'\\''") .. "'"
|
||||
local cmd = string.format(
|
||||
'curl -sSL --connect-timeout 20 --max-time 30 --retry 3 %s --insecure --location "%s"',
|
||||
user_agent ~= "" and ('-A "' .. user_agent .. '"') or "", -- 添加 or "" 处理 nil 情况
|
||||
url:gsub('["$`\\]', '\\%0') -- 安全转义
|
||||
'curl -sSL --connect-timeout 20 --max-time 30 --retry 3 -H "Accept-Encoding: identity" %s --insecure --location %s',
|
||||
ua_opt,
|
||||
safe_url
|
||||
)
|
||||
|
||||
-- 执行命令并获取输出
|
||||
local stdout = luci.sys.exec(cmd)
|
||||
stdout = trim(stdout)
|
||||
local md5 = md5_string(stdout)
|
||||
stdout = trim(stdout) -- 确保 trim 函数存在
|
||||
local md5 = md5_string(stdout) -- 确保 md5_string 函数存在
|
||||
return stdout, md5
|
||||
end
|
||||
|
||||
@ -1498,13 +1578,43 @@ local execute = function()
|
||||
end
|
||||
end
|
||||
end)
|
||||
for k, v in ipairs(nodeResult) do
|
||||
for kk, vv in ipairs(v) do
|
||||
-- 1583-1620 行为生成 sid
|
||||
-- 记录已使用编号
|
||||
local used_sid = {}
|
||||
local next_sid = 1
|
||||
-- 扫描已有 section
|
||||
ucic:foreach(name, uciType, function(s)
|
||||
local num = s[".name"]:match("^cfg(%x%x)") -- 提取两位十六进制序号
|
||||
if num then
|
||||
local n = tonumber(num, 16)
|
||||
used_sid[n] = true
|
||||
end
|
||||
end)
|
||||
-- 获取下一个可用编号(O(1))
|
||||
local function get_next_sid()
|
||||
while used_sid[next_sid] do
|
||||
next_sid = next_sid + 1
|
||||
end
|
||||
used_sid[next_sid] = true
|
||||
return next_sid
|
||||
end
|
||||
for _, v in ipairs(nodeResult) do
|
||||
for _, vv in ipairs(v) do
|
||||
if not vv._ignore then
|
||||
local section = ucic:add(name, uciType)
|
||||
ucic:tset(name, section, vv)
|
||||
ucic:set(name, section, "switch_enable", switch)
|
||||
add = add + 1
|
||||
local sid = ucic:add(name, uciType)
|
||||
if sid then
|
||||
local suffix = sid:sub(-4)
|
||||
ucic:delete(name, sid)
|
||||
local id = get_next_sid()
|
||||
local cfgid = string.format("cfg%02x%s", id, suffix)
|
||||
local section = ucic:section(name, uciType, cfgid)
|
||||
if section then
|
||||
ucic:tset(name, section, vv)
|
||||
ucic:set(name, section, "switch_enable", switch)
|
||||
add = add + 1
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user