⛄ Sync 2026-11-04 03:23:17
No files matched your search
@@ -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,37 +0,0 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-qosmate
|
||||
PKG_VERSION:=1.0.14
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_MAINTAINER:=Markus Hütter <mh@hudra.net>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=LuCI support for QoSmate
|
||||
LUCI_DEPENDS:=+qosmate +luci-lib-jsonc +lua
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PO_LANG:=de
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/qosmate
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/*.js $(1)/www/luci-static/resources/view/qosmate/
|
||||
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-qosmate.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-qosmate.json $(1)/usr/share/rpcd/acl.d/
|
||||
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.qosmate $(1)/usr/libexec/rpcd/
|
||||
endef
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -1,31 +0,0 @@
|
||||
# luci-app-qosmate
|
||||
|
||||
This repository uses the central README from the [qosmate repository](https://github.com/hudra0/qosmate).
|
||||
|
||||
|
||||
|
||||
|
||||
#### Install one-liner:
|
||||
|
||||
```bash
|
||||
mkdir -p /www/luci-static/resources/view/qosmate /usr/share/luci/menu.d /usr/share/rpcd/acl.d /usr/libexec/rpcd && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/settings.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/settings.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/hfsc.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/hfsc.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/cake.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/cake.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/advanced.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/advanced.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/rules.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/rules.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/connections.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/connections.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/custom_rules.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/custom_rules.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/ipsets.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/ipsets.js && \
|
||||
wget -O /www/luci-static/resources/view/qosmate/statistics.js https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/htdocs/luci-static/resources/view/statistics.js && \
|
||||
wget -O /usr/share/luci/menu.d/luci-app-qosmate.json https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/root/usr/share/luci/menu.d/luci-app-qosmate.json && \
|
||||
wget -O /usr/share/rpcd/acl.d/luci-app-qosmate.json https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/root/usr/share/rpcd/acl.d/luci-app-qosmate.json && \
|
||||
wget -O /usr/libexec/rpcd/luci.qosmate https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/root/usr/libexec/rpcd/luci.qosmate && \
|
||||
wget -O /usr/libexec/rpcd/luci.qosmate_stats https://raw.githubusercontent.com/hudra0/luci-app-qosmate/main/root/usr/libexec/rpcd/luci.qosmate_stats && \
|
||||
chmod +x /usr/libexec/rpcd/luci.qosmate && \
|
||||
chmod +x /usr/libexec/rpcd/luci.qosmate_stats && \
|
||||
/etc/init.d/rpcd restart && \
|
||||
/etc/init.d/uhttpd restart
|
||||
# End of command - Press Enter after pasting
|
||||
|
||||
```
|
||||
@@ -1,348 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require form';
|
||||
|
||||
var callQoSmateConntrackDSCP = rpc.declare({
|
||||
object: 'luci.qosmate',
|
||||
method: 'getConntrackDSCP',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var dscpToString = function(mark) {
|
||||
var dscp = mark & 0x3F;
|
||||
var dscpMap = {
|
||||
0: 'CS0',
|
||||
8: 'CS1',
|
||||
10: 'AF11',
|
||||
12: 'AF12',
|
||||
14: 'AF13',
|
||||
16: 'CS2',
|
||||
18: 'AF21',
|
||||
20: 'AF22',
|
||||
22: 'AF23',
|
||||
24: 'CS3',
|
||||
26: 'AF31',
|
||||
28: 'AF32',
|
||||
30: 'AF33',
|
||||
32: 'CS4',
|
||||
34: 'AF41',
|
||||
36: 'AF42',
|
||||
38: 'AF43',
|
||||
40: 'CS5',
|
||||
46: 'EF',
|
||||
48: 'CS6',
|
||||
56: 'CS7'
|
||||
};
|
||||
return dscpMap[dscp] || dscp.toString();
|
||||
};
|
||||
|
||||
var formatSize = function(bytes) {
|
||||
var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
if (bytes == 0) return '0 B';
|
||||
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
var convertToKbps = function(bytesPerSecond) {
|
||||
return (bytesPerSecond * 8 / 1000).toFixed(2) + ' Kbit/s';
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
pollInterval: 1,
|
||||
lastData: {},
|
||||
filter: '',
|
||||
sortColumn: 'bytes',
|
||||
sortDescending: true,
|
||||
connectionHistory: {},
|
||||
historyLength: 10,
|
||||
lastUpdateTime: 0,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callQoSmateConntrackDSCP()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var view = this;
|
||||
var connections = [];
|
||||
if (data[0] && data[0].connections) {
|
||||
connections = Object.values(data[0].connections);
|
||||
}
|
||||
|
||||
var filterInput = E('input', {
|
||||
'type': 'text',
|
||||
'placeholder': _('Filter by IP, IP:Port, Port, Protocol or DSCP'),
|
||||
'style': 'margin-bottom: 10px; width: 300px;',
|
||||
'value': view.filter
|
||||
});
|
||||
|
||||
filterInput.addEventListener('input', function(ev) {
|
||||
view.filter = ev.target.value.toLowerCase();
|
||||
view.updateTable(connections);
|
||||
});
|
||||
|
||||
var table = E('table', { 'class': 'table cbi-section-table', 'id': 'qosmate_connections' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'protocol') }, [ _('Protocol'), this.createSortIndicator('protocol') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'src') }, [ _('Source'), this.createSortIndicator('src') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'dst') }, [ _('Destination'), this.createSortIndicator('dst') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'dscp') }, [ _('DSCP'), this.createSortIndicator('dscp') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'bytes') }, [ _('Bytes'), this.createSortIndicator('bytes') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'packets') }, [ _('Packets'), this.createSortIndicator('packets') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'avgPps') }, [ _('Avg PPS'), this.createSortIndicator('avgPps') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'maxPps') }, [ _('Max PPS'), this.createSortIndicator('maxPps') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'avgBps') }, [ _('Avg BPS'), this.createSortIndicator('avgBps') ]))
|
||||
])
|
||||
]);
|
||||
|
||||
view.updateTable = function(connections) {
|
||||
// Remove all rows except the header
|
||||
while (table.rows.length > 1) {
|
||||
table.deleteRow(1);
|
||||
}
|
||||
|
||||
var currentTime = Date.now() / 1000;
|
||||
var timeDiff = currentTime - view.lastUpdateTime;
|
||||
view.lastUpdateTime = currentTime;
|
||||
|
||||
connections.forEach(function(conn) {
|
||||
var key = conn.layer3 + conn.protocol + conn.src + conn.sport + conn.dst + conn.dport;
|
||||
var lastConn = view.lastData[key];
|
||||
|
||||
if (!view.connectionHistory[key]) {
|
||||
view.connectionHistory[key] = {
|
||||
inPpsHistory: [],
|
||||
outPpsHistory: [],
|
||||
inBpsHistory: [],
|
||||
outBpsHistory: [],
|
||||
lastInPackets: conn.in_packets,
|
||||
lastOutPackets: conn.out_packets,
|
||||
lastInBytes: conn.in_bytes,
|
||||
lastOutBytes: conn.out_bytes,
|
||||
lastTimestamp: currentTime
|
||||
};
|
||||
}
|
||||
|
||||
var history = view.connectionHistory[key];
|
||||
var instantInPps = 0, instantOutPps = 0, instantInBps = 0, instantOutBps = 0;
|
||||
|
||||
if (lastConn && timeDiff > 0) {
|
||||
var inPacketDiff = Math.max(0, conn.in_packets - history.lastInPackets);
|
||||
var outPacketDiff = Math.max(0, conn.out_packets - history.lastOutPackets);
|
||||
var inBytesDiff = Math.max(0, conn.in_bytes - history.lastInBytes);
|
||||
var outBytesDiff = Math.max(0, conn.out_bytes - history.lastOutBytes);
|
||||
|
||||
instantInPps = Math.round(inPacketDiff / timeDiff);
|
||||
instantOutPps = Math.round(outPacketDiff / timeDiff);
|
||||
instantInBps = Math.round(inBytesDiff / timeDiff);
|
||||
instantOutBps = Math.round(outBytesDiff / timeDiff);
|
||||
|
||||
history.inPpsHistory.push(instantInPps);
|
||||
history.outPpsHistory.push(instantOutPps);
|
||||
history.inBpsHistory.push(instantInBps);
|
||||
history.outBpsHistory.push(instantOutBps);
|
||||
|
||||
if (history.inPpsHistory.length > view.historyLength) {
|
||||
history.inPpsHistory.shift();
|
||||
history.outPpsHistory.shift();
|
||||
history.inBpsHistory.shift();
|
||||
history.outBpsHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
history.lastInPackets = conn.in_packets;
|
||||
history.lastOutPackets = conn.out_packets;
|
||||
history.lastInBytes = conn.in_bytes;
|
||||
history.lastOutBytes = conn.out_bytes;
|
||||
history.lastTimestamp = currentTime;
|
||||
|
||||
var avgInPps = Math.round(history.inPpsHistory.reduce((a, b) => a + b, 0) / history.inPpsHistory.length) || 0;
|
||||
var avgOutPps = Math.round(history.outPpsHistory.reduce((a, b) => a + b, 0) / history.outPpsHistory.length) || 0;
|
||||
var avgInBps = Math.round(history.inBpsHistory.reduce((a, b) => a + b, 0) / history.inBpsHistory.length) || 0;
|
||||
var avgOutBps = Math.round(history.outBpsHistory.reduce((a, b) => a + b, 0) / history.outBpsHistory.length) || 0;
|
||||
var maxInPps = Math.max(...history.inPpsHistory, 0);
|
||||
var maxOutPps = Math.max(...history.outPpsHistory, 0);
|
||||
|
||||
conn.avgInPps = avgInPps;
|
||||
conn.avgOutPps = avgOutPps;
|
||||
conn.maxInPps = maxInPps;
|
||||
conn.maxOutPps = maxOutPps;
|
||||
conn.avgInBps = avgInBps;
|
||||
conn.avgOutBps = avgOutBps;
|
||||
view.lastData[key] = conn;
|
||||
});
|
||||
|
||||
connections.sort(view.sortFunction.bind(view));
|
||||
|
||||
connections.forEach(function(conn) {
|
||||
var srcFull = conn.src + ':' + (conn.sport || '-');
|
||||
var dstFull = conn.dst + ':' + (conn.dport || '-');
|
||||
var dscpString = dscpToString(conn.dscp);
|
||||
|
||||
if (view.filter && !(
|
||||
conn.protocol.toLowerCase().includes(view.filter) ||
|
||||
srcFull.toLowerCase().includes(view.filter) ||
|
||||
dstFull.toLowerCase().includes(view.filter) ||
|
||||
dscpString.toLowerCase().includes(view.filter)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var srcFull = conn.src + (conn.sport !== "-" ? ':' + conn.sport : '');
|
||||
var dstFull = conn.dst + (conn.dport !== "-" ? ':' + conn.dport : '');
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, conn.protocol.toUpperCase()),
|
||||
E('td', { 'class': 'td' }, srcFull),
|
||||
E('td', { 'class': 'td' }, dstFull),
|
||||
E('td', { 'class': 'td' }, dscpString),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + formatSize(conn.in_bytes)),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + formatSize(conn.out_bytes))
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + conn.in_packets),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + conn.out_packets)
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + conn.avgInPps),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + conn.avgOutPps)
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + conn.maxInPps),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + conn.maxOutPps)
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + convertToKbps(conn.avgInBps)),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + convertToKbps(conn.avgOutBps))
|
||||
])
|
||||
)
|
||||
]));
|
||||
});
|
||||
view.updateSortIndicators();
|
||||
};
|
||||
|
||||
view.updateTable(connections);
|
||||
this.updateSortIndicators();
|
||||
|
||||
poll.add(function() {
|
||||
return callQoSmateConntrackDSCP().then(function(result) {
|
||||
if (result && result.connections) {
|
||||
view.updateTable(Object.values(result.connections));
|
||||
} else {
|
||||
console.error('Invalid data received:', result);
|
||||
}
|
||||
});
|
||||
}, view.pollInterval);
|
||||
|
||||
var style = E('style', {}, `
|
||||
.sort-indicator {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
style,
|
||||
E('h2', _('QoSmate Connections')),
|
||||
E('div', { 'style': 'margin-bottom: 10px;' }, [
|
||||
filterInput
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
table
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
sortTable: function(column, ev) {
|
||||
ev.preventDefault();
|
||||
if (this.sortColumn === column) {
|
||||
this.sortDescending = !this.sortDescending;
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDescending = true;
|
||||
}
|
||||
var connections = Object.values(this.lastData);
|
||||
this.updateTable(connections);
|
||||
this.updateSortIndicators();
|
||||
},
|
||||
|
||||
sortFunction: function(a, b) {
|
||||
var aValue, bValue;
|
||||
|
||||
switch(this.sortColumn) {
|
||||
case 'bytes':
|
||||
aValue = (a.in_bytes || 0) + (a.out_bytes || 0);
|
||||
bValue = (b.in_bytes || 0) + (b.out_bytes || 0);
|
||||
break;
|
||||
case 'packets':
|
||||
aValue = (a.in_packets || 0) + (a.out_packets || 0);
|
||||
bValue = (b.in_packets || 0) + (b.out_packets || 0);
|
||||
break;
|
||||
case 'avgPps':
|
||||
aValue = (a.avgInPps || 0) + (a.avgOutPps || 0);
|
||||
bValue = (b.avgInPps || 0) + (b.avgOutPps || 0);
|
||||
break;
|
||||
case 'maxPps':
|
||||
aValue = Math.max(a.maxInPps || 0, a.maxOutPps || 0);
|
||||
bValue = Math.max(b.maxInPps || 0, b.maxOutPps || 0);
|
||||
break;
|
||||
case 'avgBps':
|
||||
aValue = (a.avgInBps || 0) + (a.avgOutBps || 0);
|
||||
bValue = (b.avgInBps || 0) + (b.avgOutBps || 0);
|
||||
break;
|
||||
default:
|
||||
aValue = a[this.sortColumn];
|
||||
bValue = b[this.sortColumn];
|
||||
}
|
||||
|
||||
if (typeof aValue === 'string') aValue = aValue.toLowerCase();
|
||||
if (typeof bValue === 'string') bValue = bValue.toLowerCase();
|
||||
|
||||
if (aValue < bValue) return this.sortDescending ? 1 : -1;
|
||||
if (aValue > bValue) return this.sortDescending ? -1 : 1;
|
||||
return 0;
|
||||
},
|
||||
|
||||
createSortIndicator: function(column) {
|
||||
return E('span', { 'class': 'sort-indicator', 'data-column': column }, '');
|
||||
},
|
||||
|
||||
updateSortIndicators: function() {
|
||||
var indicators = document.querySelectorAll('.sort-indicator');
|
||||
indicators.forEach(function(indicator) {
|
||||
if (indicator.dataset.column === this.sortColumn) {
|
||||
indicator.textContent = this.sortDescending ? ' ▼' : ' ▲';
|
||||
} else {
|
||||
indicator.textContent = '';
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
{
|
||||
"luci-app-qosmate": {
|
||||
"description": "Grant access to QoSmate configuration",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.qosmate": [ "getConntrackDSCP", "getDscpStats" ],
|
||||
"luci": [ "getInitList", "setInitAction", "getInitActionStatus", "exec" ]
|
||||
},
|
||||
"uci": [ "qosmate" ],
|
||||
"file": {
|
||||
"/etc/qosmate.sh": [ "read" ],
|
||||
"/etc/init.d/qosmate": [ "read", "exec" ],
|
||||
"/etc/hotplug.d/iface/13-qosmateHotplug": [ "read" ],
|
||||
"/etc/config/qosmate": [ "read" ],
|
||||
"/tmp/qosmate_auto_setup_output.txt": [ "read" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci": [ "setInitAction", "exec" ]
|
||||
},
|
||||
"uci": [ "qosmate" ],
|
||||
"file": {
|
||||
"/etc/qosmate.sh": [ "write" ],
|
||||
"/etc/init.d/qosmate": [ "write" ],
|
||||
"/etc/hotplug.d/iface/13-qosmateHotplug": [ "write" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
#!/usr/bin/env lua
|
||||
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
-- Check if an IP address is private - not needed at the moment
|
||||
local function is_private_ip(ip)
|
||||
if not ip or ip == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if ip:match(":") then -- IPv6
|
||||
-- Check for ULA (fc00::/7), link-local (fe80::/10), or loopback (::1)
|
||||
return ip:match("^f[cd]") or ip:match("^fe80:") or ip == "::1"
|
||||
else -- IPv4
|
||||
local octets = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
|
||||
if not octets[1] then return false end
|
||||
local first, second = tonumber(octets[1]), tonumber(octets[2])
|
||||
-- Check for private IPv4 ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
||||
return (first == 10) or
|
||||
(first == 172 and second >= 16 and second <= 31) or
|
||||
(first == 192 and second == 168)
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse a single connection line from nf_conntrack
|
||||
local function parse_connection(line)
|
||||
local conn = {}
|
||||
conn.layer3, conn.layer4, conn.protocol, conn.timeout = line:match("(%w+)%s+(%d+)%s+(%w+)%s+(%d+)")
|
||||
if not (conn.layer3 and conn.layer4 and conn.protocol and conn.timeout) then
|
||||
return nil
|
||||
end
|
||||
|
||||
if conn.protocol == "icmp" or conn.protocol == "icmpv6" then
|
||||
-- Handle ICMP and ICMPv6 protocols
|
||||
conn.src, conn.dst, conn.type, conn.code, conn.id =
|
||||
line:match("src=(%S+)%s+dst=(%S+)%s+type=(%d+)%s+code=(%d+)%s+id=(%d+)")
|
||||
conn.in_packets, conn.in_bytes = line:match("packets=(%d+)%s+bytes=(%d+)")
|
||||
conn.out_packets, conn.out_bytes = conn.in_packets, conn.in_bytes
|
||||
conn.sport, conn.dport = "-", "-" -- ICMP doesn't use ports
|
||||
else
|
||||
-- Handle other protocols (e.g., TCP, UDP)
|
||||
local src1, dst1, sport1, dport1, packets1, bytes1,
|
||||
src2, dst2, sport2, dport2, packets2, bytes2 =
|
||||
line:match("src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+).*src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+)")
|
||||
|
||||
-- Always use the first direction as the original
|
||||
conn.src, conn.dst, conn.sport, conn.dport = src1, dst1, sport1, dport1
|
||||
conn.out_packets, conn.in_packets = tonumber(packets1), tonumber(packets2)
|
||||
conn.out_bytes, conn.in_bytes = tonumber(bytes1), tonumber(bytes2)
|
||||
end
|
||||
|
||||
if not (conn.src and conn.dst) then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Extract additional connection information
|
||||
conn.dscp = math.floor(tonumber(line:match("mark=(%d+)") or "0"))
|
||||
conn.use = tonumber(line:match("use=(%d+)") or "0")
|
||||
conn.timeout = tonumber(conn.timeout) or 0
|
||||
|
||||
-- Determine connection state
|
||||
if line:match("%[ASSURED%]") then
|
||||
conn.state = "ASSURED"
|
||||
elseif line:match("%[UNREPLIED%]") then
|
||||
conn.state = "UNREPLIED"
|
||||
else
|
||||
conn.state = "UNKNOWN"
|
||||
end
|
||||
|
||||
return conn
|
||||
end
|
||||
|
||||
-- Retrieve all current connections from nf_conntrack
|
||||
local function get_connections()
|
||||
local connections = {}
|
||||
local f = io.open("/proc/net/nf_conntrack", "r")
|
||||
if not f then
|
||||
return connections
|
||||
end
|
||||
|
||||
for line in f:lines() do
|
||||
local conn = parse_connection(line)
|
||||
if conn then
|
||||
local key = string.format("%s:%s:%s:%s:%s:%s", conn.layer3, conn.protocol, conn.src, conn.sport, conn.dst, conn.dport)
|
||||
connections[key] = conn
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
|
||||
return connections
|
||||
end
|
||||
|
||||
-- Define RPC methods
|
||||
local methods = {
|
||||
getConntrackDSCP = {
|
||||
call = function()
|
||||
local connections = get_connections()
|
||||
return {result = jsonc.stringify({connections = connections})}
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
-- Handle RPC calls
|
||||
if arg[1] == "list" then
|
||||
local rv = {}
|
||||
for _, v in pairs(methods) do
|
||||
rv[_] = v.args or {}
|
||||
end
|
||||
print((jsonc.stringify(rv):gsub(":%[%]", ":{}")))
|
||||
elseif arg[1] == "call" then
|
||||
local args = jsonc.parse(io.stdin:read("*a"))
|
||||
local method = methods[arg[2]]
|
||||
if method then
|
||||
local result = method.call(args)
|
||||
print(result.result)
|
||||
os.exit(result.code or 0)
|
||||
else
|
||||
print(jsonc.stringify({error = "Method not found"}))
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"admin/network/qosmate": {
|
||||
"title": "QoSmate",
|
||||
"order": 70,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-qosmate" ],
|
||||
"uci": { "qosmate": true }
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/settings": {
|
||||
"title": "Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/settings"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/hfsc": {
|
||||
"title": "HFSC",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/hfsc"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/cake": {
|
||||
"title": "CAKE",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/cake"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/advanced": {
|
||||
"title": "Advanced",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/advanced"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/rules": {
|
||||
"title": "Rules",
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/rules"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/connections": {
|
||||
"title": "Connections",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/connections"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# all fetched files and directories *inside* directories on the left side of '='
|
||||
# will be moved into directories on the right side of '=' during installation
|
||||
# (all of this inside the distribution directory, before copying files to permanent storage)
|
||||
/root/usr/share/luci/menu.d=/usr/share/luci/menu.d
|
||||
/root/usr/share/rpcd/acl.d=/usr/share/rpcd/acl.d
|
||||
/root/usr/libexec/rpcd=/usr/libexec/rpcd
|
||||
/htdocs/luci-static/resources/view=/www/luci-static/resources/view/qosmate
|
||||
@@ -1,3 +0,0 @@
|
||||
# all files on the left side of '='
|
||||
# will be moved to paths on the right side of '=' during installation
|
||||
# (all of this inside the distribution directory, before copying files to permanent storage)
|
||||
@@ -1,425 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
// SFO warning for dynamic rule parameters
|
||||
function addSfoWarning(description, paramName) {
|
||||
var dynamicParams = [
|
||||
'UDP_RATE_LIMIT_ENABLED',
|
||||
'TCP_UPGRADE_ENABLED',
|
||||
'TCP_DOWNPRIO_INITIAL_ENABLED',
|
||||
'TCP_DOWNPRIO_SUSTAINED_ENABLED'
|
||||
];
|
||||
|
||||
if (dynamicParams.includes(paramName)) {
|
||||
var sfoEnabled = uci.get('firewall', '@defaults[0]', 'flow_offloading') === '1';
|
||||
if (sfoEnabled) {
|
||||
return description + ' ⚠ May not work with Software Flow Offloading enabled';
|
||||
}
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return Promise.all([
|
||||
uci.load('qosmate'),
|
||||
uci.load('firewall')
|
||||
]).then(() => {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate Advanced Settings'), _('Configure advanced settings for QoSmate.'));
|
||||
|
||||
// Link Layer Settings
|
||||
s = m.section(form.NamedSection, 'advanced', 'advanced', _('Link Layer Settings'),
|
||||
_('Configure link layer adaptation settings. For detailed information about different link types and overhead calculations, please check the ') +
|
||||
'<a href="https://openwrt.org/docs/guide-user/network/traffic-shaping/sqm-details" target="_blank" style="color: #1976d2; text-decoration: none;">OpenWrt SQM Documentation</a>.');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.ListValue, 'COMMON_LINK_PRESETS', _('Link Type'),
|
||||
_('Select your connection type for overhead calculation. Used by all QDiscs (HTB, HFSC, Hybrid, CAKE).'));
|
||||
o.value('ethernet', _('Ethernet (HFSC/HTB: 40B, CAKE: 40B override)'));
|
||||
o.value('docsis', _('Cable DOCSIS (25B)'));
|
||||
o.value('atm', _('DSL ATM/ADSL (44B)'));
|
||||
// CAKE native presets
|
||||
o.value('cake-ethernet', _('[CAKE] Ethernet (38B native)'));
|
||||
o.value('raw', _('[CAKE] Raw (No overhead)'));
|
||||
o.value('conservative', _('[CAKE] Conservative (48B + ATM)'));
|
||||
o.value('pppoa-vcmux', _('PPPoA VC-Mux'));
|
||||
o.value('pppoa-llc', _('PPPoA LLC'));
|
||||
o.value('pppoe-vcmux', _('PPPoE VC-Mux'));
|
||||
o.value('pppoe-llcsnap', _('PPPoE LLC-SNAP'));
|
||||
o.value('bridged-vcmux', _('Bridged VC-Mux'));
|
||||
o.value('bridged-llcsnap', _('Bridged LLC-SNAP'));
|
||||
o.value('ipoa-vcmux', _('IPoA VC-Mux'));
|
||||
o.value('ipoa-llcsnap', _('IPoA LLC-SNAP'));
|
||||
o.value('pppoe-ptm', _('PPPoE PTM'));
|
||||
o.value('bridged-ptm', _('Bridged PTM'));
|
||||
o.default = 'ethernet';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'OVERHEAD', _('Manual Overhead (bytes)'),
|
||||
_('Override automatic overhead. For HFSC/HTB: Ethernet=40, DOCSIS=25, ATM=44. CAKE will add this to its preset.'));
|
||||
o.placeholder = 'Auto';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'MPU', _('MPU'),
|
||||
_('Minimum packet unit size. Used primarily by CAKE, ignored by other QDiscs.'));
|
||||
o.placeholder = 'Default: based on preset';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'LINK_COMPENSATION', _('Link Compensation'),
|
||||
_('Additional link compensation (atm, ptm, noatm). Affects overhead calculations.'));
|
||||
o.placeholder = 'Default: based on preset';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'ETHER_VLAN_KEYWORD', _('Ether VLAN Keyword'),
|
||||
_('Ethernet VLAN keyword for CAKE. Ignored by other QDiscs.'));
|
||||
o.placeholder = 'Default: none';
|
||||
o.rmempty = true;
|
||||
|
||||
// Advanced Settings
|
||||
s = m.section(form.NamedSection, 'advanced', 'advanced', _('Advanced Settings'));
|
||||
s.anonymous = true;
|
||||
|
||||
function createOption(name, title, description, placeholder, datatype) {
|
||||
var opt = s.option(form.Value, name, title, description);
|
||||
opt.datatype = datatype || 'string';
|
||||
opt.rmempty = true;
|
||||
opt.placeholder = placeholder;
|
||||
|
||||
if (datatype === 'uinteger') {
|
||||
opt.validate = function(section_id, value) {
|
||||
if (value === '' || value === null) return true;
|
||||
if (!/^\d+$/.test(value)) return _('Must be a non-negative integer or empty');
|
||||
return true;
|
||||
};
|
||||
}
|
||||
if (datatype === 'integer') {
|
||||
opt.validate = function(section_id, value) {
|
||||
if (value === '' || value === null) return true;
|
||||
if (!/^-?\d+$/.test(value)) return _('Must be an integer or empty');
|
||||
return true;
|
||||
};
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
o = s.option(form.Flag, 'PRESERVE_CONFIG_FILES', _('Preserve Config Files'), _('Preserve configuration files during system upgrade'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'WASHDSCPUP', _('Wash DSCP Egress'), _('Sets DSCP to CS0 for outgoing packets after classification'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'WASHDSCPDOWN', _('Wash DSCP Ingress'), _('Sets DSCP to CS0 for incoming packets before classification'));
|
||||
o.rmempty = false;
|
||||
|
||||
createOption('BWMAXRATIO', _('Bandwidth Max Ratio'), _('Max download/upload ratio to prevent upstream congestion'), _('Default: 20'), 'uinteger');
|
||||
createOption('ACKRATE', _('ACK Rate'), _('Sets rate limit for TCP ACKs, helps prevent ACK flooding / set to 0 to disable ACK rate limit'), _('Default: 5% of UPRATE'), 'uinteger');
|
||||
|
||||
o = s.option(form.Flag, 'UDP_RATE_LIMIT_ENABLED', _('Enable UDP Rate Limit'), _(addSfoWarning('Downgrades UDP traffic exceeding 450 pps to lower priority', 'UDP_RATE_LIMIT_ENABLED')));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'TCP_UPGRADE_ENABLED', _('Boost Low-Volume TCP Traffic'), _(addSfoWarning('Upgrade DSCP to AF42 for TCP connections with less than 150 packets per second. This can improve responsiveness for interactive TCP services like SSH, web browsing, and instant messaging.', 'TCP_UPGRADE_ENABLED')));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'TCP_DOWNPRIO_INITIAL_ENABLED', _('Enable Initial TCP Down-Prioritization'), _(addSfoWarning('Downgrades the first ~500ms of TCP traffic (except CS1) to CS0 to prevent initial bursts', 'TCP_DOWNPRIO_INITIAL_ENABLED')));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'TCP_DOWNPRIO_SUSTAINED_ENABLED', _('Enable Sustained TCP Down-Prioritization'), _(addSfoWarning('Downgrades TCP flows exceeding ~10 seconds worth of data transfer to CS1 (Bulk). Helps prevent large downloads from starving other traffic.', 'TCP_DOWNPRIO_SUSTAINED_ENABLED')));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
createOption('UDPBULKPORT', _('UDP Bulk Ports'), _('Specify UDP ports for bulk traffic'), _('Default: none'));
|
||||
createOption('TCPBULKPORT', _('TCP Bulk Ports'), _('Specify TCP ports for bulk traffic'), _('Default: none'));
|
||||
|
||||
o = s.option(form.Value, 'MSS', _('TCP MSS'), _('Maximum Segment Size for TCP connections. This setting is only active when the upload or download bandwidth is less than 3000 kbit/s. Leave empty to use the default value. Valid range: 536-1500'), _('Default: 536'), 'uinteger');
|
||||
o.placeholder = 'Default: 536';
|
||||
o.validate = function(section_id, value) {
|
||||
if (value === '' || value === null)
|
||||
return true;
|
||||
|
||||
if (!/^\d+$/.test(value))
|
||||
return _('Must be a number');
|
||||
|
||||
let num = Number(value);
|
||||
if (num < 536 || num > 1500)
|
||||
return _('Must be between 536 and 1500');
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.option(form.ListValue, 'NFT_HOOK', _('Nftables Hook'), _('Select the nftables hook point for the dscptag chain'));
|
||||
o.value('forward', _('forward'));
|
||||
o.value('postrouting', _('postrouting'));
|
||||
o.default = 'forward';
|
||||
o.rmempty = false;
|
||||
|
||||
createOption('NFT_PRIORITY', _('Nftables Priority'), _('Set the priority for the nftables chain. Lower values are processed earlier. Default is 0 | mangle is -150.'), _('0'), 'integer');
|
||||
|
||||
// Autorate Settings Section
|
||||
s = m.section(form.NamedSection, 'autorate', 'autorate', _('Autorate Settings'),
|
||||
_('Fine-tune the dynamic bandwidth adjustment algorithm. Enable Autorate in the Settings tab.'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'interval', _('Measurement Interval (ms)'),
|
||||
_('How often to measure latency and adjust bandwidth.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '500';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'latency_increase_threshold', _('Latency Increase Threshold (ms)'),
|
||||
_('Increase bandwidth when latency delta is below this value.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '5';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'latency_decrease_threshold', _('Latency Decrease Threshold (ms)'),
|
||||
_('Decrease bandwidth when latency delta exceeds this value.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '10';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'reflectors', _('Ping Reflectors'),
|
||||
_('Space-separated list of IP addresses for latency measurement.'));
|
||||
o.placeholder = '1.1.1.1 8.8.8.8 9.9.9.9';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'refractory_increase', _('Refractory Period - Increase (s)'),
|
||||
_('Minimum wait time after a rate increase before next adjustment.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '3';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'refractory_decrease', _('Refractory Period - Decrease (s)'),
|
||||
_('Minimum wait time after a rate decrease before next adjustment.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '1';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'adjust_up_factor', _('Adjustment Factor - Increase (%)'),
|
||||
_('Multiply current rate by this percentage when increasing (102 = 2% increase).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '102';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'adjust_down_factor', _('Adjustment Factor - Decrease (%)'),
|
||||
_('Multiply current rate by this percentage when decreasing (85 = 15% decrease).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '85';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Flag, 'log_changes', _('Log All Rate Changes'),
|
||||
_('Log every rate change to system log (for debugging). May spam logs.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'min_ul_rate', _('Min Upload Rate (kbps)'),
|
||||
_('Leave empty for auto-calculation (25% of configured upload rate).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Auto');
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'base_ul_rate', _('Base Upload Rate (kbps)'),
|
||||
_('Leave empty for auto-calculation (100% of configured upload rate).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Auto');
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'max_ul_rate', _('Max Upload Rate (kbps)'),
|
||||
_('Leave empty for auto-calculation (105% of configured upload rate).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Auto');
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'min_dl_rate', _('Min Download Rate (kbps)'),
|
||||
_('Leave empty for auto-calculation (25% of configured download rate).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Auto');
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'base_dl_rate', _('Base Download Rate (kbps)'),
|
||||
_('Leave empty for auto-calculation (100% of configured download rate).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Auto');
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'max_dl_rate', _('Max Download Rate (kbps)'),
|
||||
_('Leave empty for auto-calculation (105% of configured download rate).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('Auto');
|
||||
o.rmempty = true;
|
||||
|
||||
// Update settings
|
||||
s = m.section(form.NamedSection, 'global', 'global', _('Update Settings'));
|
||||
s.anonymous = true;
|
||||
|
||||
// Display current update channels
|
||||
o = s.option(form.DummyValue, '_update_channels', _('Current Update Channels'));
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = function(section_id) {
|
||||
return null;
|
||||
};
|
||||
o.write = function() {};
|
||||
o.remove = function() {};
|
||||
o.renderWidget = function() {
|
||||
return Promise.all([
|
||||
fs.exec_direct('/etc/init.d/qosmate', ['check_version'])
|
||||
]).then(function(res) {
|
||||
var output = res[0];
|
||||
|
||||
const backendChannelMatch = output.match(/Backend versions:[\s\S]*?Update channel: (.+)/);
|
||||
const frontendChannelMatch = output.match(/Frontend versions:[\s\S]*?Update channel: (.+)/);
|
||||
|
||||
var backendChannel = backendChannelMatch ? backendChannelMatch[1].trim() : 'Unknown';
|
||||
var frontendChannel = frontendChannelMatch ? frontendChannelMatch[1].trim() : 'Unknown';
|
||||
|
||||
var tableContainer = E('div', { 'class': 'stats-table-container' });
|
||||
var table = E('table', { 'class': 'table cbi-section-table' });
|
||||
|
||||
var headerRow = E('tr', { 'class': 'tr table-titles' });
|
||||
headerRow.appendChild(E('th', { 'class': 'th' }, _('Component')));
|
||||
headerRow.appendChild(E('th', { 'class': 'th' }, _('Channel')));
|
||||
table.appendChild(headerRow);
|
||||
|
||||
var backendRow = E('tr', { 'class': 'tr' });
|
||||
backendRow.appendChild(E('td', { 'class': 'td' }, _('Backend')));
|
||||
backendRow.appendChild(E('td', { 'class': 'td' }, backendChannel));
|
||||
table.appendChild(backendRow);
|
||||
|
||||
var frontendRow = E('tr', { 'class': 'tr' });
|
||||
frontendRow.appendChild(E('td', { 'class': 'td' }, _('Frontend')));
|
||||
frontendRow.appendChild(E('td', { 'class': 'td' }, frontendChannel));
|
||||
table.appendChild(frontendRow);
|
||||
|
||||
tableContainer.appendChild(table);
|
||||
|
||||
return E('div', {}, [
|
||||
tableContainer,
|
||||
E('div', { 'style': 'margin-top: 10px' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-neutral',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
ui.showModal(_('Change Update Channel'), [
|
||||
E('p', {}, _('Select the update channel for QoSmate:')),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('div', { 'class': 'cbi-value-title' }, _('Channel')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('select', { 'id': 'change_channel', 'class': 'cbi-input-select' }, [
|
||||
E('option', {
|
||||
'value': 'release',
|
||||
'selected': backendChannel === 'release' && frontendChannel === 'release'
|
||||
}, _('Release (stable)')),
|
||||
E('option', {
|
||||
'value': 'snapshot',
|
||||
'selected': backendChannel === 'snapshot' && frontendChannel === 'snapshot'
|
||||
}, _('Snapshot (latest from main)'))
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
var channelSelect = document.querySelector('.modal [id="change_channel"]');
|
||||
var selectedChannel = channelSelect ? channelSelect.value : 'release';
|
||||
|
||||
ui.showModal(_('Changing Update Channel'), [
|
||||
E('p', { 'class': 'spinning' }, _('Please wait while the update channel is being changed...'))
|
||||
]);
|
||||
|
||||
// Need to update both components to change channel
|
||||
var updateArgs = ['update', '-f', '-v', selectedChannel];
|
||||
|
||||
console.log('Executing channel change:', '/etc/init.d/qosmate', updateArgs);
|
||||
|
||||
return fs.exec_direct('/etc/init.d/qosmate', updateArgs)
|
||||
.then(function(result) {
|
||||
console.log('Channel change result:', result);
|
||||
|
||||
if (result && (result.includes('error') || result.includes('Error') || result.includes('failed'))) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Update channel change encountered issues: %s').format(result)), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Update channel changed to %s. Reloading page...').format(selectedChannel)), 'success');
|
||||
|
||||
window.setTimeout(function() {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Channel change error:', err);
|
||||
ui.hideModal();
|
||||
|
||||
var errorMessage = _('Failed to change update channel');
|
||||
if (err) {
|
||||
if (typeof err === 'string') {
|
||||
errorMessage += ': ' + err;
|
||||
} else if (err.message) {
|
||||
errorMessage += ': ' + err.message;
|
||||
} else {
|
||||
errorMessage += ': ' + JSON.stringify(err);
|
||||
}
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', errorMessage), 'error');
|
||||
});
|
||||
})
|
||||
}, _('Change Channel'))
|
||||
])
|
||||
]);
|
||||
})
|
||||
}, _('Change Update Channel'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
return m.render();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,199 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
|
||||
// Helper function to add relevance info to descriptions
|
||||
function addRelevanceInfo(description, settingName, rootQdisc) {
|
||||
var isRelevant = true;
|
||||
var note = '';
|
||||
|
||||
// A rate of 0 disables that direction, which makes its options ineffective
|
||||
if (/INGRESS/.test(settingName) && parseInt(uci.get('qosmate', 'settings', 'DOWNRATE') || '0') === 0) {
|
||||
return description + ' ⚠ Not used: ingress shaping is disabled (DOWNRATE=0)';
|
||||
}
|
||||
if (/EGRESS/.test(settingName) && parseInt(uci.get('qosmate', 'settings', 'UPRATE') || '0') === 0) {
|
||||
return description + ' ⚠ Not used: egress shaping is disabled (UPRATE=0)';
|
||||
}
|
||||
|
||||
// Check basic ROOT_QDISC relevance
|
||||
if (rootQdisc !== 'cake' && rootQdisc !== 'hybrid') {
|
||||
isRelevant = false;
|
||||
note = ' ⚠ Not used with ' + rootQdisc.toUpperCase();
|
||||
} else {
|
||||
// Check hybrid-specific restrictions
|
||||
if (rootQdisc === 'hybrid') {
|
||||
if (settingName === 'PRIORITY_QUEUE_INGRESS' || settingName === 'PRIORITY_QUEUE_EGRESS') {
|
||||
isRelevant = false;
|
||||
note = ' ⚠ Hybrid mode uses besteffort for default class, no priority queues';
|
||||
} else if (settingName === 'AUTORATE_INGRESS') {
|
||||
isRelevant = false;
|
||||
note = ' ⚠ Hybrid mode uses besteffort for default class, no autorate';
|
||||
}
|
||||
}
|
||||
|
||||
// Generate appropriate note
|
||||
if (isRelevant) {
|
||||
if (rootQdisc === 'hybrid') {
|
||||
note = ' ✓ Active for ' + rootQdisc.toUpperCase() + ' (default traffic class)';
|
||||
} else {
|
||||
note = ' ✓ Active for ' + rootQdisc.toUpperCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return description + note;
|
||||
}
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return Promise.all([
|
||||
uci.load('qosmate'),
|
||||
fs.read('/tmp/qosmate/cake_type').then(function(res) {
|
||||
return (res || '').trim();
|
||||
}).catch(function() {
|
||||
return null;
|
||||
})
|
||||
]).then(function(data) {
|
||||
var activeCakeType = data[1];
|
||||
var m, s, o;
|
||||
var rootQdisc = uci.get('qosmate', 'settings', 'ROOT_QDISC') || 'hfsc';
|
||||
|
||||
var relevanceText = '';
|
||||
if (rootQdisc === 'cake') {
|
||||
relevanceText = _('CAKE mode active.');
|
||||
} else if (rootQdisc === 'hybrid') {
|
||||
relevanceText = _('Hybrid mode - these settings control default traffic class (normal priority).');
|
||||
} else {
|
||||
relevanceText = _('Current Root QDisc is %s - CAKE settings are not used.').format(rootQdisc.toUpperCase());
|
||||
}
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate CAKE Settings'), _('Configure CAKE settings for QoSmate.') + ' ' + relevanceText);
|
||||
|
||||
s = m.section(form.NamedSection, 'cake', 'cake', _('CAKE Settings'));
|
||||
s.anonymous = true;
|
||||
|
||||
function createOption(name, title, description, placeholder, datatype) {
|
||||
var opt = s.option(form.Value, name, title, description);
|
||||
opt.datatype = datatype || 'string';
|
||||
opt.rmempty = true;
|
||||
opt.placeholder = placeholder;
|
||||
|
||||
if (datatype === 'uinteger') {
|
||||
opt.validate = function(section_id, value) {
|
||||
if (value === '' || value === null) return true;
|
||||
if (!/^\d+$/.test(value)) return _('Must be a non-negative integer or empty');
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Add relevance info to description
|
||||
opt.description = addRelevanceInfo(description, name, rootQdisc);
|
||||
|
||||
return opt;
|
||||
}
|
||||
|
||||
o = s.option(form.ListValue, 'PRIORITY_QUEUE_INGRESS', _('Priority Queue (Ingress)'),
|
||||
addRelevanceInfo(_('Sets CAKE\'s diffserv mode for incoming traffic'), 'PRIORITY_QUEUE_INGRESS', rootQdisc));
|
||||
o.value('diffserv3', _('Diffserv 3-tier priority'));
|
||||
o.value('diffserv4', _('Diffserv 4-tier priority'));
|
||||
o.value('diffserv8', _('Diffserv 8-tier priority'));
|
||||
o.default = 'diffserv4';
|
||||
|
||||
o = s.option(form.ListValue, 'PRIORITY_QUEUE_EGRESS', _('Priority Queue (Egress)'),
|
||||
addRelevanceInfo(_('Sets CAKE\'s diffserv mode for outgoing traffic'), 'PRIORITY_QUEUE_EGRESS', rootQdisc));
|
||||
o.value('diffserv3', _('Diffserv 3-tier priority'));
|
||||
o.value('diffserv4', _('Diffserv 4-tier priority'));
|
||||
o.value('diffserv8', _('Diffserv 8-tier priority'));
|
||||
o.default = 'diffserv4';
|
||||
|
||||
o = s.option(form.Flag, 'HOST_ISOLATION', _('Host Isolation'),
|
||||
addRelevanceInfo(_('Applies fairness first by host, then by flow(dual-srchost/dual-dsthost)'), 'HOST_ISOLATION', rootQdisc));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'NAT_INGRESS', _('NAT (Ingress)'),
|
||||
addRelevanceInfo(_('Enable NAT lookup for ingress'), 'NAT_INGRESS', rootQdisc));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'NAT_EGRESS', _('NAT (Egress)'),
|
||||
addRelevanceInfo(_('Enable NAT lookup for egress'), 'NAT_EGRESS', rootQdisc));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.ListValue, 'ACK_FILTER_EGRESS', _('ACK Filter (Egress)'),
|
||||
addRelevanceInfo(_('Set ACK filter for egress. Auto enables filtering if download/upload ratio ≥ 15.'), 'ACK_FILTER_EGRESS', rootQdisc));
|
||||
o.value('auto', _('Auto'));
|
||||
o.value('1', _('Enable'));
|
||||
o.value('0', _('Disable'));
|
||||
o.default = 'auto';
|
||||
|
||||
createOption('RTT', _('RTT'), _('Set the Round Trip Time'), _('Default: auto'), 'uinteger');
|
||||
|
||||
o = s.option(form.Flag, 'AUTORATE_INGRESS', _('Autorate (Ingress)'),
|
||||
addRelevanceInfo(_('Enable autorate for ingress'), 'AUTORATE_INGRESS', rootQdisc));
|
||||
o.rmempty = false;
|
||||
o.default = '0';
|
||||
|
||||
createOption('EXTRA_PARAMETERS_INGRESS', _('Extra Parameters (Ingress)'), _('Set extra parameters for ingress'), _('Default: none'));
|
||||
createOption('EXTRA_PARAMETERS_EGRESS', _('Extra Parameters (Egress)'), _('Set extra parameters for egress'), _('Default: none'));
|
||||
|
||||
var mqDesc = _('Use cake_mq for multi-core CPU scaling. Only effective in pure CAKE mode (not hybrid). Requires kernel support (OpenWrt 25.12+).');
|
||||
var mqStatus = '';
|
||||
var useMq = uci.get('qosmate', 'cake', 'USE_MQ') || '0';
|
||||
if (rootQdisc === 'hybrid') {
|
||||
mqStatus = ' ⚠ ' + _('Not available in Hybrid mode (cake_mq requires root qdisc)');
|
||||
} else if (rootQdisc !== 'cake') {
|
||||
mqStatus = ' ⚠ ' + _('Not used with %s').format(rootQdisc.toUpperCase());
|
||||
} else if (useMq === '1') {
|
||||
if (activeCakeType === 'cake_mq') {
|
||||
mqStatus = ' ✓ ' + _('cake_mq is active');
|
||||
} else if (activeCakeType === 'cake') {
|
||||
mqStatus = ' ⚠ ' + _('cake_mq not available on this system, using standard cake');
|
||||
} else {
|
||||
mqStatus = ' ℹ ' + _('Service not running - restart to apply');
|
||||
}
|
||||
}
|
||||
|
||||
o = s.option(form.ListValue, 'USE_MQ', _('Multi-Queue CAKE (cake_mq)'), mqDesc + mqStatus);
|
||||
o.value('0', _('Off (default)'));
|
||||
o.value('1', _('Auto-detect'));
|
||||
o.default = '0';
|
||||
|
||||
return m.render();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,627 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
var callQoSmateConntrackDSCP = rpc.declare({
|
||||
object: 'luci.qosmate',
|
||||
method: 'getConntrackDSCP',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var dscpToString = function(mark) {
|
||||
var dscp = mark & 0x3F;
|
||||
var dscpMap = {
|
||||
0: 'CS0',
|
||||
8: 'CS1',
|
||||
10: 'AF11',
|
||||
12: 'AF12',
|
||||
14: 'AF13',
|
||||
16: 'CS2',
|
||||
18: 'AF21',
|
||||
20: 'AF22',
|
||||
22: 'AF23',
|
||||
24: 'CS3',
|
||||
26: 'AF31',
|
||||
28: 'AF32',
|
||||
30: 'AF33',
|
||||
32: 'CS4',
|
||||
34: 'AF41',
|
||||
36: 'AF42',
|
||||
38: 'AF43',
|
||||
40: 'CS5',
|
||||
46: 'EF',
|
||||
48: 'CS6',
|
||||
56: 'CS7'
|
||||
};
|
||||
return dscpMap[dscp] || dscp.toString();
|
||||
};
|
||||
|
||||
var formatSize = function(bytes) {
|
||||
var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
if (bytes == 0) return '0 B';
|
||||
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
var convertToKbps = function(bytesPerSecond) {
|
||||
return (bytesPerSecond * 8 / 1000).toFixed(2) + ' Kbit/s';
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
pollInterval: 1,
|
||||
lastData: {},
|
||||
filter: '',
|
||||
sortColumn: 'bytes',
|
||||
sortDescending: true,
|
||||
connectionHistory: {},
|
||||
historyLength: 10,
|
||||
lastUpdateTime: 0,
|
||||
autoRefresh: true,
|
||||
refreshTimeout: null,
|
||||
hasPolledOnce: false,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callQoSmateConntrackDSCP(), { connections: {} }),
|
||||
uci.load('qosmate')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var view = this;
|
||||
var connections = [];
|
||||
var max_connections = 0;
|
||||
|
||||
if (data[0] && data[0].connections) {
|
||||
connections = Object.values(data[0].connections);
|
||||
max_connections = data[0].max_connections || 0;
|
||||
}
|
||||
|
||||
// Get current UCI value for dropdown
|
||||
var current_uci_limit = uci.get('qosmate', 'advanced', 'MAX_CONNECTIONS') || '0';
|
||||
|
||||
var filterInput = E('input', {
|
||||
'type': 'text',
|
||||
'placeholder': _('Filter by: IP IP:Port Port Protocol DSCP'),
|
||||
'style': 'margin-bottom: 10px; width: 300px;',
|
||||
'value': view.filter
|
||||
});
|
||||
|
||||
filterInput.addEventListener('input', function(ev) {
|
||||
view.filter = ev.target.value.toLowerCase();
|
||||
view.updateTable(connections);
|
||||
});
|
||||
|
||||
// Create connection limit dropdown
|
||||
var limitSelect = E('select', {
|
||||
'id': 'connection_limit_select',
|
||||
'style': 'margin-left: 10px;',
|
||||
'change': function(ev) {
|
||||
var newLimit = parseInt(ev.target.value);
|
||||
|
||||
applyConnectionLimit(newLimit);
|
||||
}
|
||||
}, [
|
||||
E('option', { 'value': '0' }, _('Unlimited')),
|
||||
E('option', { 'value': '10' }, _('10')),
|
||||
E('option', { 'value': '50' }, _('50')),
|
||||
E('option', { 'value': '100' }, _('100')),
|
||||
E('option', { 'value': '500' }, _('500')),
|
||||
E('option', { 'value': '1000' }, _('1000')),
|
||||
E('option', { 'value': '2000' }, _('2000')),
|
||||
E('option', { 'value': '5000' }, _('5000'))
|
||||
]);
|
||||
|
||||
// Set current value from UCI
|
||||
limitSelect.value = current_uci_limit;
|
||||
|
||||
// Function to apply connection limit directly
|
||||
function applyConnectionLimit(newLimit) {
|
||||
return uci.load('qosmate').then(function() {
|
||||
uci.set('qosmate', 'advanced', 'MAX_CONNECTIONS', newLimit.toString());
|
||||
return uci.save();
|
||||
}).then(function() {
|
||||
return uci.apply();
|
||||
}).then(function() {
|
||||
uci.unload('qosmate');
|
||||
return uci.load('qosmate');
|
||||
}).then(function() {
|
||||
setTimeout(function() {
|
||||
view.load().then(function(newData) {
|
||||
var newConnections = [];
|
||||
var newMaxConnections = 0;
|
||||
if (newData[0] && newData[0].connections) {
|
||||
newConnections = Object.values(newData[0].connections);
|
||||
newMaxConnections = newData[0].max_connections || 0;
|
||||
}
|
||||
view.updateTable(newConnections);
|
||||
view.updateLimitWarning(newMaxConnections, newConnections.length);
|
||||
|
||||
// Update dropdown to reflect current value
|
||||
limitSelect.value = newMaxConnections.toString();
|
||||
});
|
||||
}, 1000);
|
||||
}).catch(function(error) {
|
||||
console.error('Error applying UCI changes:', error);
|
||||
// Show error but don't persist notification
|
||||
ui.addNotification(null, E('p', _('Failed to apply connection limit: %s').format(error.message || error)), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Create limit warning (initially hidden)
|
||||
var limitWarning = E('div', {
|
||||
'id': 'limit_warning',
|
||||
'style': 'background-color: #fff3cd; color: #856404; padding: 10px; margin: 10px 0; border-radius: 5px; display: none;'
|
||||
});
|
||||
|
||||
view.updateLimitWarning = function(maxConnections, currentCount) {
|
||||
if (maxConnections > 0) {
|
||||
limitWarning.innerHTML = '⚠️ ' + _('Limited to %d connections. Some connections may not be shown.').format(maxConnections);
|
||||
limitWarning.style.display = 'block';
|
||||
} else {
|
||||
limitWarning.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
var table = E('table', { 'class': 'table cbi-section-table', 'id': 'qosmate_connections' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'protocol') }, [ _('Protocol'), this.createSortIndicator('protocol') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'src') }, [ _('Source'), this.createSortIndicator('src') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'dst') }, [ _('Destination'), this.createSortIndicator('dst') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'dscp') }, [ _('DSCP'), this.createSortIndicator('dscp') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'bytes') }, [ _('Bytes'), this.createSortIndicator('bytes') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'packets') }, [ _('Packets'), this.createSortIndicator('packets') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'avgPps') }, [ _('Avg PPS'), this.createSortIndicator('avgPps') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'maxPps') }, [ _('Max PPS'), this.createSortIndicator('maxPps') ])),
|
||||
E('th', { 'class': 'th' }, E('a', { 'href': '#', 'click': this.sortTable.bind(this, 'avgBps') }, [ _('Avg BPS'), this.createSortIndicator('avgBps') ]))
|
||||
])
|
||||
]);
|
||||
|
||||
view.showErrorMessage = function(message) {
|
||||
// Clear table and show error message
|
||||
while (table.rows.length > 1) {
|
||||
table.deleteRow(1);
|
||||
}
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '9', 'style': 'text-align: center; color: red; padding: 20px;' },
|
||||
message)
|
||||
]));
|
||||
};
|
||||
|
||||
view.updateTable = function(connections) {
|
||||
try {
|
||||
// Remove all rows except the header
|
||||
while (table.rows.length > 1) {
|
||||
table.deleteRow(1);
|
||||
}
|
||||
|
||||
var currentTime = Date.now() / 1000;
|
||||
var timeDiff = currentTime - view.lastUpdateTime;
|
||||
view.lastUpdateTime = currentTime;
|
||||
|
||||
connections.forEach(function(conn) {
|
||||
var key = conn.layer3 + conn.protocol + conn.src + conn.sport + conn.dst + conn.dport;
|
||||
var lastConn = view.lastData[key];
|
||||
|
||||
if (!view.connectionHistory[key]) {
|
||||
view.connectionHistory[key] = {
|
||||
inPpsHistory: [],
|
||||
outPpsHistory: [],
|
||||
inBpsHistory: [],
|
||||
outBpsHistory: [],
|
||||
lastInPackets: conn.in_packets,
|
||||
lastOutPackets: conn.out_packets,
|
||||
lastInBytes: conn.in_bytes,
|
||||
lastOutBytes: conn.out_bytes,
|
||||
lastTimestamp: currentTime
|
||||
};
|
||||
}
|
||||
|
||||
var history = view.connectionHistory[key];
|
||||
var instantInPps = 0, instantOutPps = 0, instantInBps = 0, instantOutBps = 0;
|
||||
|
||||
if (lastConn && timeDiff > 0) {
|
||||
var inPacketDiff = Math.max(0, conn.in_packets - history.lastInPackets);
|
||||
var outPacketDiff = Math.max(0, conn.out_packets - history.lastOutPackets);
|
||||
var inBytesDiff = Math.max(0, conn.in_bytes - history.lastInBytes);
|
||||
var outBytesDiff = Math.max(0, conn.out_bytes - history.lastOutBytes);
|
||||
|
||||
instantInPps = Math.round(inPacketDiff / timeDiff);
|
||||
instantOutPps = Math.round(outPacketDiff / timeDiff);
|
||||
instantInBps = Math.round(inBytesDiff / timeDiff);
|
||||
instantOutBps = Math.round(outBytesDiff / timeDiff);
|
||||
|
||||
history.inPpsHistory.push(instantInPps);
|
||||
history.outPpsHistory.push(instantOutPps);
|
||||
history.inBpsHistory.push(instantInBps);
|
||||
history.outBpsHistory.push(instantOutBps);
|
||||
|
||||
if (history.inPpsHistory.length > view.historyLength) {
|
||||
history.inPpsHistory.shift();
|
||||
history.outPpsHistory.shift();
|
||||
history.inBpsHistory.shift();
|
||||
history.outBpsHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
history.lastInPackets = conn.in_packets;
|
||||
history.lastOutPackets = conn.out_packets;
|
||||
history.lastInBytes = conn.in_bytes;
|
||||
history.lastOutBytes = conn.out_bytes;
|
||||
history.lastTimestamp = currentTime;
|
||||
|
||||
var avgInPps = Math.round(history.inPpsHistory.reduce((a, b) => a + b, 0) / history.inPpsHistory.length) || 0;
|
||||
var avgOutPps = Math.round(history.outPpsHistory.reduce((a, b) => a + b, 0) / history.outPpsHistory.length) || 0;
|
||||
var avgInBps = Math.round(history.inBpsHistory.reduce((a, b) => a + b, 0) / history.inBpsHistory.length) || 0;
|
||||
var avgOutBps = Math.round(history.outBpsHistory.reduce((a, b) => a + b, 0) / history.outBpsHistory.length) || 0;
|
||||
var maxInPps = Math.max(...history.inPpsHistory, 0);
|
||||
var maxOutPps = Math.max(...history.outPpsHistory, 0);
|
||||
|
||||
conn.avgInPps = avgInPps;
|
||||
conn.avgOutPps = avgOutPps;
|
||||
conn.maxInPps = maxInPps;
|
||||
conn.maxOutPps = maxOutPps;
|
||||
conn.avgInBps = avgInBps;
|
||||
conn.avgOutBps = avgOutBps;
|
||||
view.lastData[key] = conn;
|
||||
});
|
||||
|
||||
connections.sort(view.sortFunction.bind(view));
|
||||
|
||||
connections.forEach(function(conn) {
|
||||
if (view.filter) {
|
||||
// Split the filter string by whitespace to get individual tokens
|
||||
var tokens = view.filter.split(/\s+/).map(function(token) {
|
||||
return token.trim().toLowerCase();
|
||||
});
|
||||
|
||||
// Collect the relevant fields for matching
|
||||
var dscpString = dscpToString(conn.dscp);
|
||||
var srcFull = conn.src + (conn.sport !== "-" ? ':' + conn.sport : '');
|
||||
var dstFull = conn.dst + (conn.dport !== "-" ? ':' + conn.dport : '');
|
||||
var fields = [
|
||||
conn.protocol.toLowerCase(),
|
||||
srcFull.toLowerCase(),
|
||||
dstFull.toLowerCase(),
|
||||
dscpString.toLowerCase()
|
||||
];
|
||||
|
||||
// Each token must match at least one field (AND logic across tokens, OR logic across fields)
|
||||
var pass = tokens.every(function(t) {
|
||||
return fields.some(function(field) {
|
||||
return field.includes(t);
|
||||
});
|
||||
});
|
||||
if (!pass) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var srcFull = conn.src + ':' + (conn.sport || '-');
|
||||
var dstFull = conn.dst + ':' + (conn.dport || '-');
|
||||
var dscpString = dscpToString(conn.dscp);
|
||||
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, conn.protocol.toUpperCase()),
|
||||
E('td', { 'class': 'td' }, srcFull),
|
||||
E('td', { 'class': 'td' }, dstFull),
|
||||
E('td', { 'class': 'td' }, dscpString),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + formatSize(conn.in_bytes)),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + formatSize(conn.out_bytes))
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + conn.in_packets),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + conn.out_packets)
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + conn.avgInPps),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + conn.avgOutPps)
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + conn.maxInPps),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + conn.maxOutPps)
|
||||
])
|
||||
),
|
||||
E('td', { 'class': 'td' },
|
||||
E('div', {}, [
|
||||
E('span', {}, _('In: ') + convertToKbps(conn.avgInBps)),
|
||||
E('br'),
|
||||
E('span', {}, _('Out: ') + convertToKbps(conn.avgOutBps))
|
||||
])
|
||||
)
|
||||
]));
|
||||
});
|
||||
view.updateSortIndicators();
|
||||
|
||||
// Update connection count display
|
||||
var connectionCountDisplay = document.getElementById('connection_count_display');
|
||||
if (connectionCountDisplay) {
|
||||
connectionCountDisplay.textContent = _('Connections: ') + connections.length;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error updating table:', e);
|
||||
// Show error message instead of crashing
|
||||
while (table.rows.length > 1) {
|
||||
table.deleteRow(1);
|
||||
}
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '9', 'style': 'text-align: center; color: red;' },
|
||||
_('Error displaying connections. System may be overloaded.'))
|
||||
]));
|
||||
|
||||
// Update connection count display for error case
|
||||
var connectionCountDisplay = document.getElementById('connection_count_display');
|
||||
if (connectionCountDisplay) {
|
||||
connectionCountDisplay.textContent = _('Connections: Error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view.updateTable(connections);
|
||||
view.updateLimitWarning(max_connections, connections.length);
|
||||
this.updateSortIndicators();
|
||||
|
||||
// Trigger the adaptive polling:
|
||||
adaptivePoll(view);
|
||||
|
||||
var style = E('style', {}, `
|
||||
.sort-indicator {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
.cbi-section-table {
|
||||
min-width: 100%;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.cbi-section-table td, .cbi-section-table th {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.cbi-section-table td:nth-child(5),
|
||||
.cbi-section-table th:nth-child(5),
|
||||
.cbi-section-table td:nth-child(6),
|
||||
.cbi-section-table th:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Styles for different zoom levels */
|
||||
.cbi-section-table.zoom-100 { font-size: 1rem !important; }
|
||||
.cbi-section-table.zoom-90 { font-size: 0.9rem !important; }
|
||||
.cbi-section-table.zoom-80 { font-size: 0.8rem !important; }
|
||||
.cbi-section-table.zoom-70 { font-size: 0.7rem !important; }
|
||||
.cbi-section-table.zoom-60 { font-size: 0.6rem !important; }
|
||||
.cbi-section-table.zoom-50 { font-size: 0.5rem !important; }
|
||||
|
||||
/* Adjust padding for zoomed states */
|
||||
.cbi-section-table[class*="zoom-"] td,
|
||||
.cbi-section-table[class*="zoom-"] th {
|
||||
padding: 0.2rem !important;
|
||||
}
|
||||
|
||||
/* Style for the zoom select */
|
||||
.zoom-select {
|
||||
margin-left: 10px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
`);
|
||||
|
||||
// Create zoom select
|
||||
var zoomSelect = E('select', {
|
||||
'class': 'zoom-select',
|
||||
'change': function(ev) {
|
||||
var table = document.getElementById('qosmate_connections');
|
||||
// Remove all zoom classes
|
||||
table.classList.remove('zoom-100', 'zoom-90', 'zoom-80', 'zoom-70', 'zoom-60', 'zoom-50');
|
||||
// Add selected zoom class
|
||||
table.classList.add(ev.target.value);
|
||||
}
|
||||
}, [
|
||||
E('option', { 'value': 'zoom-100' }, _('100%')),
|
||||
E('option', { 'value': 'zoom-90' }, _('90%')),
|
||||
E('option', { 'value': 'zoom-80' }, _('80%')),
|
||||
E('option', { 'value': 'zoom-70' }, _('70%')),
|
||||
E('option', { 'value': 'zoom-60' }, _('60%')),
|
||||
E('option', { 'value': 'zoom-50' }, _('50%'))
|
||||
]);
|
||||
|
||||
// Display the current polling interval.
|
||||
var pollIntervalDisplay = E('span', {
|
||||
'id': 'poll_interval_display',
|
||||
'style': 'margin-left: 10px;'
|
||||
}, _('Polling Interval: ') + this.pollInterval + ' s');
|
||||
|
||||
// Display connection count
|
||||
var connectionCountDisplay = E('span', {
|
||||
'id': 'connection_count_display',
|
||||
'style': 'float: right; margin-left: 10px; font-weight: bold; line-height: 2.5em;'
|
||||
}, _('Connections: 0'));
|
||||
|
||||
// Include limit elements in the top container
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
style,
|
||||
E('h2', _('QoSmate Connections')),
|
||||
limitWarning,
|
||||
E('div', { 'style': 'margin-bottom: 10px;' }, [
|
||||
filterInput,
|
||||
' ',
|
||||
E('span', _('Max Connections:')),
|
||||
limitSelect,
|
||||
' ',
|
||||
E('span', _('Zoom:')),
|
||||
zoomSelect,
|
||||
pollIntervalDisplay,
|
||||
connectionCountDisplay,
|
||||
' ',
|
||||
E('button', {
|
||||
'type': 'button',
|
||||
'style': 'margin-left: 10px;',
|
||||
'click': function(ev) {
|
||||
if (view.autoRefresh) {
|
||||
clearTimeout(view.refreshTimeout);
|
||||
view.autoRefresh = false;
|
||||
this.textContent = _('Resume');
|
||||
} else {
|
||||
view.autoRefresh = true;
|
||||
this.textContent = _('Pause');
|
||||
adaptivePoll(view);
|
||||
}
|
||||
}
|
||||
}, _('Pause'))
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
table
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
sortTable: function(column, ev) {
|
||||
ev.preventDefault();
|
||||
if (this.sortColumn === column) {
|
||||
this.sortDescending = !this.sortDescending;
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDescending = true;
|
||||
}
|
||||
|
||||
// Defer sorting + table update to reduce blocking in click event handler
|
||||
var view = this;
|
||||
setTimeout(function() {
|
||||
var connections = Object.values(view.lastData);
|
||||
view.updateTable(connections);
|
||||
view.updateSortIndicators();
|
||||
}, 0);
|
||||
},
|
||||
|
||||
sortFunction: function(a, b) {
|
||||
var aValue, bValue;
|
||||
|
||||
switch(this.sortColumn) {
|
||||
case 'bytes':
|
||||
aValue = (a.in_bytes || 0) + (a.out_bytes || 0);
|
||||
bValue = (b.in_bytes || 0) + (b.out_bytes || 0);
|
||||
break;
|
||||
case 'packets':
|
||||
aValue = (a.in_packets || 0) + (a.out_packets || 0);
|
||||
bValue = (b.in_packets || 0) + (b.out_packets || 0);
|
||||
break;
|
||||
case 'avgPps':
|
||||
aValue = (a.avgInPps || 0) + (a.avgOutPps || 0);
|
||||
bValue = (b.avgInPps || 0) + (b.avgOutPps || 0);
|
||||
break;
|
||||
case 'maxPps':
|
||||
aValue = Math.max(a.maxInPps || 0, a.maxOutPps || 0);
|
||||
bValue = Math.max(b.maxInPps || 0, b.maxOutPps || 0);
|
||||
break;
|
||||
case 'avgBps':
|
||||
aValue = (a.avgInBps || 0) + (a.avgOutBps || 0);
|
||||
bValue = (b.avgInBps || 0) + (b.avgOutBps || 0);
|
||||
break;
|
||||
default:
|
||||
aValue = a[this.sortColumn];
|
||||
bValue = b[this.sortColumn];
|
||||
}
|
||||
|
||||
if (typeof aValue === 'string') aValue = aValue.toLowerCase();
|
||||
if (typeof bValue === 'string') bValue = bValue.toLowerCase();
|
||||
|
||||
if (aValue < bValue) return this.sortDescending ? 1 : -1;
|
||||
if (aValue > bValue) return this.sortDescending ? -1 : 1;
|
||||
return 0;
|
||||
},
|
||||
|
||||
createSortIndicator: function(column) {
|
||||
return E('span', { 'class': 'sort-indicator', 'data-column': column }, '');
|
||||
},
|
||||
|
||||
updateSortIndicators: function() {
|
||||
var indicators = document.querySelectorAll('.sort-indicator');
|
||||
indicators.forEach(function(indicator) {
|
||||
if (indicator.dataset.column === this.sortColumn) {
|
||||
indicator.textContent = this.sortDescending ? ' ▼' : ' ▲';
|
||||
} else {
|
||||
indicator.textContent = '';
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
|
||||
// Adaptive polling function that measures response time and adjusts the polling interval.
|
||||
function adaptivePoll(view) {
|
||||
if (!view.autoRefresh) {
|
||||
return; // Do not schedule a new poll if auto-refresh is paused
|
||||
}
|
||||
var startTime = Date.now();
|
||||
L.resolveDefault(callQoSmateConntrackDSCP(), { connections: {} }).then(function(result) {
|
||||
var responseTime = Date.now() - startTime;
|
||||
|
||||
// Adjust the polling interval based on response time.
|
||||
if (!view.hasPolledOnce) {
|
||||
view.pollInterval = 3;
|
||||
view.hasPolledOnce = true;
|
||||
} else if (responseTime > 2000) { // If response time exceeds 2000ms, increase interval.
|
||||
view.pollInterval = Math.min(view.pollInterval + 1, 10); // Max poll interval of 10 seconds.
|
||||
} else if (responseTime < 1000 && view.pollInterval > 1) { // If response time is less than 1000ms and poll interval is greater than 1 second, decrease interval.
|
||||
view.pollInterval = Math.max(view.pollInterval - 1, 1); // Min poll interval of 1 second.
|
||||
}
|
||||
// Update the polling interval display in the UI.
|
||||
var pollDisplay = document.getElementById('poll_interval_display');
|
||||
if (pollDisplay) {
|
||||
pollDisplay.textContent = _('Polling Interval: ') + view.pollInterval + ' s';
|
||||
}
|
||||
|
||||
if (result && result.connections) {
|
||||
var connections = Object.values(result.connections);
|
||||
var max_connections = result.max_connections || 0;
|
||||
|
||||
view.updateTable(connections);
|
||||
view.updateLimitWarning(max_connections, connections.length);
|
||||
} else {
|
||||
console.warn('Invalid data received:', result);
|
||||
// Show error message to user
|
||||
view.showErrorMessage('No connection data received');
|
||||
}
|
||||
}).catch(function(error) {
|
||||
console.error('Polling error:', error);
|
||||
// Show error message to user instead of keeping old data
|
||||
view.showErrorMessage('Connection error - check network');
|
||||
}).finally(function() {
|
||||
// Schedule the next poll only if auto-refresh is not paused
|
||||
if (view.autoRefresh) {
|
||||
view.refreshTimeout = setTimeout(function() {
|
||||
adaptivePoll(view);
|
||||
}, view.pollInterval * 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => {
|
||||
return ui.changes.apply();
|
||||
})
|
||||
.then(() => {
|
||||
return callInitAction('qosmate', 'restart');
|
||||
})
|
||||
.then(() => {
|
||||
ui.addNotification(null, E('p', _('All rules have been saved and applied.')), 'success');
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or restart QoSmate: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
fs.read('/etc/qosmate.d/custom_rules.nft')
|
||||
.then(content => {
|
||||
content = content.replace(/^table\s+inet\s+qosmate_custom\s*{/, '');
|
||||
content = content.replace(/}\s*$/, '');
|
||||
return content.trim();
|
||||
})
|
||||
.catch(() => ''),
|
||||
fs.read('/etc/qosmate.d/inline_dscptag.nft')
|
||||
.catch(() => ''),
|
||||
fs.read('/tmp/qosmate_custom_rules_validation.txt')
|
||||
.catch(() => '')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function([customRules, inlineRules, validationResult]) {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate Custom Rules'),
|
||||
_('Define custom nftables rules for advanced traffic control.'));
|
||||
|
||||
s = m.section(form.NamedSection, 'custom_rules', 'qosmate', _('Custom Rules'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Button, '_erase', _('Erase Rules'));
|
||||
o.inputstyle = 'remove';
|
||||
o.inputtitle = _('Erase Custom Rules');
|
||||
o.onclick = function(ev) {
|
||||
return ui.showModal(_('Erase Custom Rules'), [
|
||||
E('p', _('Are you sure you want to erase all custom rules? This action cannot be undone.')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-negative',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
var customTextarea = document.querySelector('textarea[name="cbid.qosmate.custom_rules.custom_rules"]');
|
||||
if (customTextarea) {
|
||||
customTextarea.value = '';
|
||||
}
|
||||
|
||||
var inlineTextarea = document.querySelector('textarea[name="cbid.qosmate.custom_rules.inline_rules"]');
|
||||
if (inlineTextarea) {
|
||||
inlineTextarea.value = '';
|
||||
}
|
||||
|
||||
return fs.remove('/etc/qosmate.d/custom_rules.nft')
|
||||
.then(() => {
|
||||
return fs.remove('/etc/qosmate.d/inline_dscptag.nft');
|
||||
})
|
||||
.then(() => {
|
||||
return ui.changes.apply();
|
||||
})
|
||||
.then(() => {
|
||||
return callInitAction('qosmate', 'restart');
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('All rules have been erased and changes applied.')), 'success');
|
||||
window.setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to erase custom rules or apply changes: ') + err.message), 'error');
|
||||
});
|
||||
})
|
||||
}, _('Erase'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
o = s.option(form.TextValue, 'custom_rules', _('Custom nftables Rules'));
|
||||
o.rows = 10;
|
||||
o.wrap = 'off';
|
||||
o.rmempty = true;
|
||||
o.monospace = true;
|
||||
o.datatype = 'string';
|
||||
o.description = _('Enter your custom nftables rules here. The "table inet qosmate_custom { ... }" wrapper will be added automatically.') +
|
||||
'<div style="margin-top: 8px;">' +
|
||||
'<button type="button" onclick="toggleExample(\'custom\')" class="btn cbi-button" style="font-size: 11px; padding: 3px 6px;">' +
|
||||
'▼ ' + _('Show Examples') + '</button>' +
|
||||
'<div id="custom-example" class="cbi-section-node" style="display: none; margin-top: 8px;">' +
|
||||
'<strong>' + _('Example (Full Table Rules):') + '</strong><br/>' +
|
||||
'<pre style="background:rgba(255,255,255,0.1); border:1px solid rgba(128,128,128,0.3); padding:6px; margin:4px 0; border-radius:3px; font-size:11px; white-space:pre-wrap; font-family:monospace;">' +
|
||||
'chain forward {\n' +
|
||||
' type filter hook forward priority 0; policy accept;\n' +
|
||||
' # Mark high-rate TCP traffic from specific IP\n' +
|
||||
' ip saddr 192.168.138.100 tcp flags & (fin|syn|rst|ack) != 0\n' +
|
||||
' limit rate over 300/second burst 300 packets\n' +
|
||||
' counter ip dscp set cs1\n' +
|
||||
'}' +
|
||||
'</pre></div></div>';
|
||||
o.load = function(section_id) {
|
||||
return customRules;
|
||||
};
|
||||
o.write = function(section_id, formvalue) {
|
||||
// Prepare the new custom rules (only the table definition)
|
||||
const newRules = `table inet qosmate_custom {
|
||||
${formvalue.trim()}
|
||||
}`;
|
||||
|
||||
// Delete the existing table before applying new rules
|
||||
// If deletion fails (table doesn't exist), ignore the error
|
||||
return fs.exec('nft', ['delete', 'table', 'inet', 'qosmate_custom'])
|
||||
.catch(() => { /* ignore deletion error */ })
|
||||
.then(() => fs.write('/etc/qosmate.d/custom_rules.nft', newRules))
|
||||
.then(() => fs.exec('/etc/init.d/qosmate', ['validate_custom_rules']))
|
||||
.then(() => fs.read('/tmp/qosmate_custom_rules_validation.txt'))
|
||||
.then((result) => {
|
||||
if (result.includes('Overall validation: PASSED')) {
|
||||
ui.addNotification(null, E('p', _('Rules validation successful.')), 'success');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Rules validation failed. Please check the validation result below.')), 'warning');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
o = s.option(form.TextValue, 'inline_rules', _('Inline Extra Rules'));
|
||||
o.rows = 10;
|
||||
o.wrap = 'off';
|
||||
o.rmempty = true;
|
||||
o.monospace = true;
|
||||
o.datatype = 'string';
|
||||
o.description = _('Statements only – run inside chain dscptag at hook $NFT_HOOK / priority $NFT_PRIORITY. Do not start with \'table\' or \'chain\'. Included only if validation passes.') +
|
||||
'<div style="margin-top: 8px;">' +
|
||||
'<button type="button" onclick="toggleExample(\'inline\')" class="btn cbi-button" style="font-size: 11px; padding: 3px 6px;">' +
|
||||
'▼ ' + _('Show Examples') + '</button>' +
|
||||
'<div id="inline-example" class="cbi-section-node" style="display: none; margin-top: 8px;">' +
|
||||
'<strong>' + _('Example (Inline Rules):') + '</strong><br/>' +
|
||||
'<pre style="background:rgba(255,255,255,0.1); border:1px solid rgba(128,128,128,0.3); padding:6px; margin:4px 0; border-radius:3px; font-size:11px; white-space:pre-wrap; font-family:monospace;">' +
|
||||
'# Mark traffic from specific IP as high priority\n' +
|
||||
'ip saddr 192.168.1.100 ip dscp set cs5 comment "Gaming PC priority"\n\n' +
|
||||
'# Rate limit and mark bulk TCP traffic\n' +
|
||||
'meta l4proto tcp limit rate 100/second ip dscp set cs1 comment "Bulk TCP limit"\n\n' +
|
||||
'# Mark VoIP traffic from specific port range\n' +
|
||||
'udp sport 5060-5070 ip dscp set ef comment "SIP/RTP VoIP traffic"' +
|
||||
'</pre></div></div>';
|
||||
o.load = function(section_id) {
|
||||
return inlineRules;
|
||||
};
|
||||
o.write = function(section_id, formvalue) {
|
||||
return fs.write('/etc/qosmate.d/inline_dscptag.nft', formvalue || '')
|
||||
.then(() => fs.exec('/etc/init.d/qosmate', ['validate_custom_rules']))
|
||||
.then(() => fs.read('/tmp/qosmate_custom_rules_validation.txt'))
|
||||
.then((result) => {
|
||||
if (result.includes('Overall validation: PASSED')) {
|
||||
ui.addNotification(null, E('p', _('Rules validation successful.')), 'success');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Rules validation failed. Please check the validation result below.')), 'warning');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
o = s.option(form.DummyValue, '_validation_result', _('Validation Result'));
|
||||
o.rawhtml = true;
|
||||
o.default = validationResult
|
||||
? '<div class="cbi-section-node" style="margin-top: 8px; min-width: 700px;">' +
|
||||
'<pre style="background:rgba(255,255,255,0.1); border:1px solid rgba(128,128,128,0.3); padding:6px; margin:4px 0; border-radius:3px; font-size:11px; white-space:pre-wrap; font-family:monospace;">' +
|
||||
validationResult + '</pre></div>'
|
||||
: _('No validation performed yet');
|
||||
|
||||
o = s.option(form.Button, '_validate', _('Validate Rules'));
|
||||
o.inputstyle = 'apply';
|
||||
o.inputtitle = _('Validate');
|
||||
o.onclick = function(ev) {
|
||||
var map = this.map;
|
||||
var section_id = 'custom_rules'; // Assuming this is the correct section_id
|
||||
|
||||
var customRulesTextarea = document.getElementById('widget.cbid.qosmate.' + section_id + '.custom_rules');
|
||||
var inlineRulesTextarea = document.getElementById('widget.cbid.qosmate.' + section_id + '.inline_rules');
|
||||
|
||||
if (!customRulesTextarea || !inlineRulesTextarea) {
|
||||
ui.addNotification(null, E('p', _('Error: Could not find rules textareas')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var currentCustomRules = customRulesTextarea.value;
|
||||
var currentInlineRules = inlineRulesTextarea.value;
|
||||
var fullCustomContent = `table inet qosmate_custom {\n${currentCustomRules.trim()}\n}`;
|
||||
|
||||
ui.showModal(_('Validating Rules'), [
|
||||
E('p', { 'class': 'spinning' }, _('Please wait while the rules are being validated...'))
|
||||
]);
|
||||
|
||||
return fs.write('/etc/qosmate.d/custom_rules.nft', fullCustomContent)
|
||||
.then(() => {
|
||||
return fs.write('/etc/qosmate.d/inline_dscptag.nft', currentInlineRules || '');
|
||||
})
|
||||
.then(() => {
|
||||
return fs.exec('/etc/init.d/qosmate', ['validate_custom_rules']);
|
||||
})
|
||||
.then(() => {
|
||||
return fs.read('/tmp/qosmate_custom_rules_validation.txt');
|
||||
})
|
||||
.then((result) => {
|
||||
ui.hideModal();
|
||||
if (result.includes('Overall validation: PASSED')) {
|
||||
ui.addNotification(null, E('p', _('Rules validation successful.')), 'success');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Rules validation failed. Please check the validation result below.')), 'warning');
|
||||
}
|
||||
var validationResultElement = document.getElementById('cbid.qosmate.custom_rules._validation_result');
|
||||
if (validationResultElement) {
|
||||
validationResultElement.innerHTML = '<div class="cbi-section-node" style="margin-top: 8px; min-width: 700px;">' +
|
||||
'<pre style="background:rgba(255,255,255,0.1); border:1px solid rgba(128,128,128,0.3); padding:6px; margin:4px 0; border-radius:3px; font-size:11px; white-space:pre-wrap; font-family:monospace;">' +
|
||||
result + '</pre></div>';
|
||||
}
|
||||
ui.showModal(_('Finalizing Validation'), [
|
||||
E('p', { 'class': 'spinning' }, _('Finalizing validation results, please wait...'))
|
||||
]);
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error during validation: ') + err), 'error');
|
||||
|
||||
ui.showModal(_('Finalizing Validation'), [
|
||||
E('p', { 'class': 'spinning' }, _('Finalizing validation results, please wait...'))
|
||||
]);
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// Add toggle functionality
|
||||
if (typeof window.toggleExample === 'undefined') {
|
||||
window.toggleExample = function(type) {
|
||||
var element = document.getElementById(type + '-example');
|
||||
var button = event.target;
|
||||
if (element.style.display === 'none') {
|
||||
element.style.display = 'block';
|
||||
button.innerHTML = '▲ ' + button.innerHTML.split(' ').slice(1).join(' ');
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
button.innerHTML = '▼ ' + button.innerHTML.split(' ').slice(1).join(' ');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,203 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
|
||||
// Helper function to add relevance info to descriptions
|
||||
function addRelevanceInfo(description, settingName, rootQdisc, gameqdisc) {
|
||||
var isRelevant = true;
|
||||
var note = '';
|
||||
|
||||
// Check basic ROOT_QDISC relevance
|
||||
if (rootQdisc !== 'hfsc' && rootQdisc !== 'hybrid') {
|
||||
isRelevant = false;
|
||||
note = ' ⚠ Not used with ' + rootQdisc.toUpperCase();
|
||||
} else {
|
||||
// Check gameqdisc-specific dependencies
|
||||
var gameqdiscDependencies = {
|
||||
'netem': ['netemdelayms', 'netemjitterms', 'netemdist', 'netem_direction', 'pktlossp'],
|
||||
'pfifo': ['PFIFOMIN', 'PACKETSIZE'], // PFIFO-specific settings
|
||||
// MAXDEL is used by multiple qdiscs, so handle separately
|
||||
};
|
||||
|
||||
// Settings that are used by multiple gameqdiscs
|
||||
var multiGameqdiscSettings = {
|
||||
'MAXDEL': ['red', 'pfifo', 'bfifo', 'drr', 'qfq'] // Used for burst/limit calculations
|
||||
};
|
||||
|
||||
// Check if setting is gameqdisc-specific
|
||||
var isGameqdiscSpecific = false;
|
||||
var requiredGameqdisc = '';
|
||||
var supportedGameqdiscs = [];
|
||||
|
||||
// Check single-gameqdisc dependencies
|
||||
for (var qdisc in gameqdiscDependencies) {
|
||||
if (gameqdiscDependencies[qdisc].includes(settingName)) {
|
||||
isGameqdiscSpecific = true;
|
||||
if (qdisc !== gameqdisc) {
|
||||
isRelevant = false;
|
||||
requiredGameqdisc = qdisc;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check multi-gameqdisc settings
|
||||
if (!isGameqdiscSpecific && multiGameqdiscSettings[settingName]) {
|
||||
supportedGameqdiscs = multiGameqdiscSettings[settingName];
|
||||
isGameqdiscSpecific = true;
|
||||
if (!supportedGameqdiscs.includes(gameqdisc)) {
|
||||
isRelevant = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check hybrid-specific settings
|
||||
if (rootQdisc === 'hybrid') {
|
||||
if (settingName === 'nongameqdisc' || settingName === 'nongameqdiscoptions') {
|
||||
isRelevant = false;
|
||||
note = ' ⚠ Hybrid mode uses CAKE for default class and fq_codel for bulk traffic';
|
||||
}
|
||||
}
|
||||
|
||||
// Generate appropriate note
|
||||
if (isRelevant) {
|
||||
if (isGameqdiscSpecific) {
|
||||
note = ' ✓ Active for ' + rootQdisc.toUpperCase() + ' + ' + gameqdisc.toUpperCase();
|
||||
} else {
|
||||
note = ' ✓ Active for ' + rootQdisc.toUpperCase();
|
||||
}
|
||||
} else {
|
||||
if (requiredGameqdisc) {
|
||||
note = ' ⚠ Only relevant for ' + requiredGameqdisc.toUpperCase() + ' gameqdisc';
|
||||
} else if (supportedGameqdiscs.length > 0) {
|
||||
var gameqdiscList = supportedGameqdiscs.map(function(q) { return q.toUpperCase(); }).join(', ');
|
||||
note = ' ⚠ Only relevant for ' + gameqdiscList + ' gameqdisc' + (supportedGameqdiscs.length > 1 ? 's' : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return description + note;
|
||||
}
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return Promise.all([
|
||||
uci.load('qosmate')
|
||||
]).then(() => {
|
||||
var m, s, o;
|
||||
var rootQdisc = uci.get('qosmate', 'settings', 'ROOT_QDISC') || 'hfsc';
|
||||
var gameqdisc = uci.get('qosmate', 'hfsc', 'gameqdisc') || 'pfifo';
|
||||
|
||||
var relevanceText = '';
|
||||
if (rootQdisc === 'hfsc') {
|
||||
relevanceText = _('HFSC mode active.');
|
||||
} else if (rootQdisc === 'hybrid') {
|
||||
relevanceText = _('Hybrid mode - these settings control realtime traffic (high priority class).');
|
||||
} else {
|
||||
relevanceText = _('Current Root QDisc is %s - HFSC settings are not used.').format(rootQdisc.toUpperCase());
|
||||
}
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate HFSC Settings'), _('Configure HFSC settings for QoSmate.') + ' ' + relevanceText);
|
||||
|
||||
s = m.section(form.NamedSection, 'hfsc', 'hfsc', _('HFSC Settings'));
|
||||
s.anonymous = true;
|
||||
|
||||
function createOption(name, title, description, placeholder, datatype) {
|
||||
var opt = s.option(form.Value, name, title, description);
|
||||
opt.datatype = datatype || 'string';
|
||||
opt.rmempty = true;
|
||||
opt.placeholder = placeholder;
|
||||
|
||||
if (datatype === 'uinteger') {
|
||||
opt.validate = function(section_id, value) {
|
||||
if (value === '' || value === null) return true;
|
||||
if (!/^\d+$/.test(value)) return _('Must be a non-negative integer or empty');
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Add relevance info to description
|
||||
opt.description = addRelevanceInfo(description, name, rootQdisc, gameqdisc);
|
||||
|
||||
return opt;
|
||||
}
|
||||
|
||||
o = s.option(form.ListValue, 'gameqdisc', _('Game Queue Discipline'),
|
||||
addRelevanceInfo(_('Queueing method for traffic classified as realtime'), 'gameqdisc', rootQdisc, gameqdisc));
|
||||
o.value('pfifo', _('PFIFO'));
|
||||
o.value('fq_codel', _('FQ_CODEL'));
|
||||
o.value('bfifo', _('BFIFO'));
|
||||
o.value('red', _('RED'));
|
||||
o.value('netem', _('NETEM'));
|
||||
o.default = 'pfifo';
|
||||
|
||||
createOption('GAMEUP', _('Game Upload (kbps)'), _('Bandwidth reserved for realtime upload traffic'), _('Default: 15% of UPRATE + 400'), 'uinteger');
|
||||
createOption('GAMEDOWN', _('Game Download (kbps)'), _('Bandwidth reserved for realtime download traffic'), _('Default: 15% of DOWNRATE + 400'), 'uinteger');
|
||||
|
||||
o = s.option(form.ListValue, 'nongameqdisc', _('Non-Game Queue Discipline'),
|
||||
addRelevanceInfo(_('Select the queueing discipline for non-realtime traffic'), 'nongameqdisc', rootQdisc, gameqdisc));
|
||||
o.value('fq_codel', _('FQ_CODEL'));
|
||||
o.value('cake', _('CAKE'));
|
||||
o.default = 'fq_codel';
|
||||
|
||||
createOption('nongameqdiscoptions', _('Non-Game QDisc Options'), _('Cake options for non-realtime queueing discipline'), _('Default: besteffort ack-filter'));
|
||||
createOption('MAXDEL', _('Max Delay (ms)'), _('Target max delay for realtime packets after burst (pfifo, bfifo, red)'), _('Default: 24'), 'uinteger');
|
||||
createOption('PFIFOMIN', _('PFIFO Min'), _('Minimum packet count for PFIFO queue'), _('Default: 5'), 'uinteger');
|
||||
createOption('PACKETSIZE', _('Avg Packet Size (B)'), _('Used with PFIFOMIN to calculate PFIFO limit'), _('Default: 450'), 'uinteger');
|
||||
createOption('netemdelayms', _('NETEM Delay (ms)'), _('NETEM delay in milliseconds'), _('Default: 30'), 'uinteger');
|
||||
createOption('netemjitterms', _('NETEM Jitter (ms)'), _('NETEM jitter in milliseconds'), _('Default: 7'), 'uinteger');
|
||||
|
||||
o = s.option(form.ListValue, 'netem_direction', _('NETEM Direction'),
|
||||
addRelevanceInfo(_('Select which direction to apply the NETEM delay/jitter settings'), 'netem_direction', rootQdisc, gameqdisc));
|
||||
o.depends('gameqdisc', 'netem');
|
||||
o.value('both', _('Both Directions'));
|
||||
o.value('egress', _('Egress Only'));
|
||||
o.value('ingress', _('Ingress Only'));
|
||||
o.default = 'both';
|
||||
|
||||
o = s.option(form.ListValue, 'netemdist', _('NETEM Distribution'),
|
||||
addRelevanceInfo(_('NETEM delay distribution'), 'netemdist', rootQdisc, gameqdisc));
|
||||
o.value('experimental', _('Experimental'));
|
||||
o.value('normal', _('Normal'));
|
||||
o.value('pareto', _('Pareto'));
|
||||
o.value('paretonormal', _('Pareto Normal'));
|
||||
o.default = 'normal';
|
||||
|
||||
createOption('pktlossp', _('Packet Loss Percentage'), _('Percentage of packet loss'), _('Default: none'));
|
||||
|
||||
return m.render();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate IP Sets'),
|
||||
_('Define groups of IP addresses that can be referenced in QoS rules using @setname'));
|
||||
|
||||
s = m.section(form.GridSection, 'ipset', _('IP Sets'));
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s.sortable = true;
|
||||
|
||||
o = s.option(form.Value, 'name', _('Set Name'));
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value) {
|
||||
return _('Set name is required');
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||||
return _('Set name must contain only letters, numbers, and underscore');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.option(form.ListValue, 'mode', _('Set Mode'));
|
||||
o.value('static', _('Static'));
|
||||
o.value('dynamic', _('Dynamic'));
|
||||
o.default = 'static';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'family', _('Family'));
|
||||
o.value('ipv4', _('IPv4'));
|
||||
o.value('ipv6', _('IPv6'));
|
||||
o.default = 'ipv4';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.DynamicList, 'ip4', _('IPv4 Addresses'));
|
||||
o.datatype = 'or(ip4addr,iprange4)';
|
||||
o.rmempty = true;
|
||||
o.placeholder = _('Add IPv4 address, subnet or range');
|
||||
o.depends({ mode: 'static', family: 'ipv4' });
|
||||
|
||||
o = s.option(form.DynamicList, 'ip6', _('IPv6 Addresses'));
|
||||
o.datatype = 'or(ip6addr,iprange6)';
|
||||
o.rmempty = true;
|
||||
o.placeholder = _('Add IPv6 address, subnet or range');
|
||||
o.depends({ mode: 'static', family: 'ipv6' });
|
||||
|
||||
o = s.option(form.Value, 'timeout', _('Timeout'));
|
||||
o.placeholder = _('e.g., 1h');
|
||||
o.depends('mode', 'dynamic');
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value)
|
||||
return true; // Allow empty value if applicable
|
||||
// Validate that the timeout is in a valid format (e.g., combination of numbers and allowed time units: h, m, s)
|
||||
if (!/^(\d+[hms])+$/.test(value)) {
|
||||
return _('Timeout must be in a valid format (e.g., "10s", "1h", "2h12m10s")');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,238 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require validation';
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
// Bridges LuCI's validation types into plain functions; see LuCI.validation docs
|
||||
var stubValidator = {
|
||||
factory: validation,
|
||||
apply: function(type, value, args) {
|
||||
if (value != null)
|
||||
this.value = value;
|
||||
return validation.types[type].apply(this, args);
|
||||
},
|
||||
assert: function(condition) { return !!condition; }
|
||||
};
|
||||
|
||||
// Validation helper for IP/IPv6 targets
|
||||
function validateTargetField(section_id, value) {
|
||||
if (!value || value.length === 0) return true;
|
||||
|
||||
var values = Array.isArray(value) ? value : value.split(/\s+/);
|
||||
var ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3})(?:\/(?:[0-9]|[1-2]\d|3[0-2]))?$/;
|
||||
var ipv6Regex = /^(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$|^(?:[A-Fa-f0-9]{1,4}:){1,7}:$|^(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}$|^(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}$|^(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}$|^(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}$|^(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}$|^[A-Fa-f0-9]{1,4}:(?::[A-Fa-f0-9]{1,4}){1,6}$|^:(?::[A-Fa-f0-9]{1,4}){1,7}$|^(?:[A-Fa-f0-9]{1,4}:){7}:$|^::$/;
|
||||
var ipv6CidrRegex = /^(?:[A-Fa-f0-9]{1,4}:){1,7}[A-Fa-f0-9]{0,4}\/(?:[0-9]|[1-9]\d|1[01]\d|12[0-8])$/;
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var v = values[i];
|
||||
|
||||
// Check for set reference
|
||||
if (v.startsWith('@') || v.startsWith('!=@')) {
|
||||
var setName = v.replace(/^(!=)?@/, '');
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(setName)) {
|
||||
return _('Invalid set name format: ') + v;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip != prefix for validation
|
||||
var isNegated = v.startsWith('!=');
|
||||
var valueToValidate = isNegated ? v.substring(2) : v;
|
||||
|
||||
// Block MAC addresses explicitly
|
||||
var macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||
if (macRegex.test(valueToValidate)) {
|
||||
return _('MAC addresses not supported: ') + v + _(' (use IP addresses instead)');
|
||||
}
|
||||
|
||||
// nftables accepts address ranges wherever a plain address is allowed
|
||||
if (valueToValidate.indexOf('-') !== -1) {
|
||||
if (!stubValidator.apply('iprange', valueToValidate)) {
|
||||
return _('Invalid IP range: ') + v;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate IP/CIDR/IPv6
|
||||
if (!ipv4Regex.test(valueToValidate) &&
|
||||
!ipv6Regex.test(valueToValidate) &&
|
||||
!ipv6CidrRegex.test(valueToValidate)) {
|
||||
return _('Invalid target format: ') + v + _(' (expected IP, IPv6, CIDR, or @setname)');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return Promise.all([
|
||||
uci.load('qosmate')
|
||||
]).then(() => {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate Rate Limits'),
|
||||
_('Configure per-device bandwidth limits using nftables meters. Limits are applied based on the source IP of devices, making it easy to control traffic for specific devices or subnets.'));
|
||||
|
||||
s = m.section(form.GridSection, 'ratelimit', _('Rate Limits'));
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s.sortable = true;
|
||||
|
||||
s.tab('general', _('General'));
|
||||
s.tab('limits', _('Limits'));
|
||||
|
||||
// General tab
|
||||
o = s.taboption('general', form.Value, 'name', _('Name'));
|
||||
o.rmempty = false;
|
||||
o.placeholder = _('e.g., Guest Network Limit');
|
||||
|
||||
o = s.taboption('general', form.DynamicList, 'target', _('Target Devices'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = _('192.168.1.100, !=192.168.1.77, 2001:db8::1');
|
||||
o.rmempty = false;
|
||||
o.validate = validateTargetField;
|
||||
o.write = function(section_id, formvalue) {
|
||||
// Ensure != prefix is properly formatted
|
||||
var values = formvalue.map(function(v) {
|
||||
return v.replace(/^!(?!=)/, '!=');
|
||||
});
|
||||
return this.super('write', [section_id, values]);
|
||||
};
|
||||
var superRenderWidgetTarget = o.renderWidget;
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var widget = superRenderWidgetTarget.call(this, section_id, option_index, cfgvalue);
|
||||
var descr = E('div', { 'class': 'cbi-value-description' },
|
||||
'IP/IPv6/CIDR addresses and subnets only. Exclusions: !=192.168.1.77, IP Sets: @vip_guests');
|
||||
return E('div', {}, [widget, descr]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
// Limits tab
|
||||
o = s.taboption('limits', form.Value, 'download_limit', _('Download Limit (Kbit/s)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('10000');
|
||||
o.default = '10000';
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value) return true;
|
||||
var intVal = parseInt(value);
|
||||
if (intVal < 0 || intVal > 100000000) {
|
||||
return _('Must be between 0 and 100000000 Kbit/s');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var superRenderWidgetDownload = o.renderWidget;
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var widget = superRenderWidgetDownload.call(this, section_id, option_index, cfgvalue);
|
||||
var descr = E('div', { 'class': 'cbi-value-description' }, _('Traffic TO device. 0 = unlimited'));
|
||||
return E('div', {}, [widget, descr]);
|
||||
};
|
||||
|
||||
o = s.taboption('limits', form.Value, 'upload_limit', _('Upload Limit (Kbit/s)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('10000');
|
||||
o.default = '10000';
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value) return true;
|
||||
var intVal = parseInt(value);
|
||||
if (intVal < 0 || intVal > 100000000) {
|
||||
return _('Must be between 0 and 100000000 Kbit/s');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
var superRenderWidgetUpload = o.renderWidget;
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var widget = superRenderWidgetUpload.call(this, section_id, option_index, cfgvalue);
|
||||
var descr = E('div', { 'class': 'cbi-value-description' }, _('Traffic FROM device. 0 = unlimited'));
|
||||
return E('div', {}, [widget, descr]);
|
||||
};
|
||||
|
||||
o = s.taboption('limits', form.Value, 'burst_factor', _('Burst Factor'));
|
||||
o.placeholder = _('1.0');
|
||||
o.default = '1.0';
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value || value === '') return true;
|
||||
|
||||
// Only allow digits, comma, and period
|
||||
if (!/^[0-9,\.]+$/.test(value)) {
|
||||
return _('Only digits and decimal separators allowed');
|
||||
}
|
||||
|
||||
// Accept both comma and period as decimal separator
|
||||
var normalizedValue = value.replace(',', '.');
|
||||
|
||||
// Validate decimal format
|
||||
if (!/^\d*\.?\d*$/.test(normalizedValue)) {
|
||||
return _('Invalid decimal format');
|
||||
}
|
||||
|
||||
var floatVal = parseFloat(normalizedValue);
|
||||
if (isNaN(floatVal) || floatVal < 0.0 || floatVal > 10.0) {
|
||||
return _('Must be between 0.0 and 10.0 (0 = no burst)');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
o.write = function(section_id, formvalue) {
|
||||
// Normalize comma to period before saving
|
||||
if (formvalue) {
|
||||
formvalue = formvalue.replace(',', '.');
|
||||
}
|
||||
return this.super('write', [section_id, formvalue]);
|
||||
};
|
||||
var superRenderWidgetBurst = o.renderWidget;
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var widget = superRenderWidgetBurst.call(this, section_id, option_index, cfgvalue);
|
||||
var descr = E('div', { 'class': 'cbi-value-description' }, _('0 = no burst (strict), 1.0 = rate as burst, higher = more burst'));
|
||||
return E('div', {}, [widget, descr]);
|
||||
};
|
||||
|
||||
// Custom modal title
|
||||
s.modaltitle = function(section_id) {
|
||||
var name = uci.get('qosmate', section_id, 'name');
|
||||
if (name) {
|
||||
return _('Edit Rate Limit: %s').format(name);
|
||||
}
|
||||
return _('Add Rate Limit');
|
||||
};
|
||||
|
||||
return m.render();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,634 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require validation';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
var callRuleCounters = rpc.declare({
|
||||
object: 'luci.qosmate_stats',
|
||||
method: 'getRuleCounters',
|
||||
expect: { rule_counters: [] }
|
||||
});
|
||||
|
||||
// Bridges LuCI's validation types into plain functions; see LuCI.validation docs
|
||||
var stubValidator = {
|
||||
factory: validation,
|
||||
apply: function(type, value, args) {
|
||||
if (value != null)
|
||||
this.value = value;
|
||||
return validation.types[type].apply(this, args);
|
||||
},
|
||||
assert: function(condition) { return !!condition; }
|
||||
};
|
||||
|
||||
// IPv6 suffix matching validation helpers
|
||||
function isIPv6SuffixFormat(value) {
|
||||
// Format: ::suffix/::mask - allow empty suffix/mask with * instead of +
|
||||
return /^::([0-9a-fA-F:]*)\/::([0-9a-fA-F:]*)$/.test(value);
|
||||
}
|
||||
|
||||
function validateIPv6Part(part) {
|
||||
// Special case: empty part is valid (for ::/::mask)
|
||||
if (part === '') return true;
|
||||
|
||||
// Check for multiple consecutive colons (:: can only appear once)
|
||||
if (/:{3,}/.test(part)) return false;
|
||||
if (/[^0-9a-fA-F:]/.test(part)) return false; // Only hex and colons allowed
|
||||
|
||||
// Check if :: appears more than once
|
||||
var doubleColonCount = (part.match(/::/g) || []).length;
|
||||
if (doubleColonCount > 1) return false;
|
||||
|
||||
// For suffix matching, we already have :: at the start, so no more :: allowed
|
||||
if (doubleColonCount > 0) return false;
|
||||
|
||||
// Split by colons and check each segment
|
||||
var segments = part.split(':');
|
||||
|
||||
// Can't have more than 8 segments (for a full IPv6)
|
||||
if (segments.length > 8) return false;
|
||||
|
||||
for (var i = 0; i < segments.length; i++) {
|
||||
var seg = segments[i];
|
||||
// Each segment must be 0-4 hex digits
|
||||
if (!/^[0-9a-fA-F]{0,4}$/.test(seg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseIPv6Suffix(value) {
|
||||
const match = value.match(/^::([0-9a-fA-F:]*)\/::([0-9a-fA-F:]*)$/);
|
||||
if (!match) return null;
|
||||
|
||||
// Validate both parts
|
||||
if (!validateIPv6Part(match[1]) || !validateIPv6Part(match[2])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
suffix: match[1],
|
||||
mask: match[2],
|
||||
isSuffixMatch: true
|
||||
};
|
||||
}
|
||||
|
||||
// Common IP validation function for src_ip and dest_ip fields
|
||||
function validateIPField(section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var values = Array.isArray(value) ? value : value.split(/\s+/);
|
||||
var ipCidrRegex = /^(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3})(?:\/(?:[0-9]|[1-2]\d|3[0-2]))?|(?:(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}|(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}|(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}|(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}|[A-Fa-f0-9]{1,4}:(?:(?::[A-Fa-f0-9]{1,4}){1,6})|:(?:(?::[A-Fa-f0-9]{1,4}){1,7}|:))(?:\/(?:[0-9]|[1-9]\d|1[0-1]\d|12[0-8]))?)$/;
|
||||
|
||||
// Check for mixed IPv4/IPv6 within this field
|
||||
var hasIPv4 = false;
|
||||
var hasIPv6 = false;
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var v = values[i].replace(/^!(?!=)/, '!=');
|
||||
|
||||
// Check for set reference
|
||||
if (v.startsWith('@') || v.startsWith('!=@')) {
|
||||
var setName = v.replace(/^(!=)?@/, '');
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(setName)) {
|
||||
return _('Invalid set name format. Must start with @ followed by letters, numbers, or underscore');
|
||||
}
|
||||
continue; // Don't check IP version for sets
|
||||
}
|
||||
|
||||
// Strip != prefix for validation
|
||||
var isNegated = v.startsWith('!=');
|
||||
var valueToValidate = isNegated ? v.substring(2) : v;
|
||||
|
||||
// Check for IPv6 suffix format
|
||||
if (isIPv6SuffixFormat(valueToValidate)) {
|
||||
var suffixData = parseIPv6Suffix(valueToValidate);
|
||||
if (!suffixData) {
|
||||
return _('Invalid IPv6 suffix format. Use ::suffix/::mask with valid IPv6 segments (e.g. ::1234:5678/::ffff:ffff)');
|
||||
}
|
||||
hasIPv6 = true;
|
||||
}
|
||||
// nftables accepts address ranges wherever a plain address is allowed
|
||||
else if (valueToValidate.indexOf('-') !== -1) {
|
||||
if (!stubValidator.apply('iprange', valueToValidate)) {
|
||||
return _('Invalid IP range: ') + v;
|
||||
}
|
||||
if (valueToValidate.indexOf(':') !== -1) {
|
||||
hasIPv6 = true;
|
||||
} else {
|
||||
hasIPv4 = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!ipCidrRegex.test(valueToValidate)) {
|
||||
return _('Invalid IP address or CIDR format: ') + v;
|
||||
}
|
||||
// Detect IP version
|
||||
if (valueToValidate.indexOf(':') !== -1) {
|
||||
hasIPv6 = true;
|
||||
} else {
|
||||
hasIPv4 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
// Rule counter polling
|
||||
counterData: {},
|
||||
pollHandler: null,
|
||||
pollInterval: 8,
|
||||
|
||||
load: function() {
|
||||
this.counterData = {};
|
||||
return this.super && this.super('load') || Promise.resolve();
|
||||
},
|
||||
|
||||
// Create polling function for rule counter updates
|
||||
createPollFunction: function() {
|
||||
var view = this;
|
||||
return function() {
|
||||
|
||||
return callRuleCounters().then(function(result) {
|
||||
|
||||
if (result && Array.isArray(result)) {
|
||||
|
||||
view.updateCounterData(result);
|
||||
} else {
|
||||
|
||||
}
|
||||
return result;
|
||||
}).catch(function(err) {
|
||||
|
||||
return null;
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
// Start counter polling
|
||||
startCounterPolling: function() {
|
||||
if (this.pollHandler) {
|
||||
return; // Already polling
|
||||
}
|
||||
|
||||
var view = this;
|
||||
var startPollingWhenReady = function() {
|
||||
var luciTable = document.querySelector('.cbi-section-table');
|
||||
|
||||
|
||||
if (luciTable) {
|
||||
|
||||
view.pollHandler = view.createPollFunction();
|
||||
poll.add(view.pollHandler, view.pollInterval);
|
||||
view.pollHandler(); // Initial call
|
||||
} else {
|
||||
|
||||
setTimeout(startPollingWhenReady, 1000); // Retry after 1 second
|
||||
}
|
||||
};
|
||||
|
||||
startPollingWhenReady();
|
||||
},
|
||||
|
||||
// Stop counter polling
|
||||
stopCounterPolling: function() {
|
||||
if (this.pollHandler) {
|
||||
poll.remove(this.pollHandler);
|
||||
this.pollHandler = null;
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
// Update counter data and refresh UI
|
||||
updateCounterData: function(ruleCounters) {
|
||||
var view = this;
|
||||
view.counterData = {};
|
||||
|
||||
if (Array.isArray(ruleCounters)) {
|
||||
ruleCounters.forEach(function(rule) {
|
||||
view.counterData[rule.rule_name] = {
|
||||
packets: rule.total_packets || 0,
|
||||
bytes: rule.total_bytes || 0,
|
||||
ipv4_packets: rule.ipv4_packets || 0,
|
||||
ipv6_packets: rule.ipv6_packets || 0
|
||||
};
|
||||
});
|
||||
view.updateActivityColumnInTable();
|
||||
}
|
||||
},
|
||||
|
||||
// Update Activity column in LuCI table
|
||||
updateActivityColumnInTable: function() {
|
||||
var view = this;
|
||||
setTimeout(function() {
|
||||
// Find LuCI table and Activity column
|
||||
var table = document.querySelector('.cbi-section-table');
|
||||
if (!table) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var headerRow = table.querySelector('tr');
|
||||
var activityColumnIndex = -1;
|
||||
|
||||
if (headerRow) {
|
||||
var headers = headerRow.querySelectorAll('th');
|
||||
headers.forEach(function(th, index) {
|
||||
if (th.textContent.includes(_('Activity'))) {
|
||||
activityColumnIndex = index;
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (activityColumnIndex >= 0) {
|
||||
var dataRows = table.querySelectorAll('tr[data-sid]');
|
||||
|
||||
|
||||
dataRows.forEach(function(row) {
|
||||
var sectionId = row.getAttribute('data-sid');
|
||||
var cells = row.querySelectorAll('td');
|
||||
if (cells[activityColumnIndex] && sectionId) {
|
||||
var ruleName = uci.get('qosmate', sectionId, 'name');
|
||||
var counterEnabled = uci.get('qosmate', sectionId, 'counter');
|
||||
|
||||
|
||||
|
||||
var activityCell = cells[activityColumnIndex];
|
||||
var content = view.formatCounterDisplay(ruleName, counterEnabled);
|
||||
activityCell.innerHTML = '';
|
||||
activityCell.appendChild(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 200); // Give LuCI time to render
|
||||
},
|
||||
|
||||
// Format counter display
|
||||
formatCounterDisplay: function(ruleName, counterEnabled) {
|
||||
if (counterEnabled !== '1') {
|
||||
return E('span', {'style': 'color: #999; font-size: 0.9em;'}, '-');
|
||||
}
|
||||
|
||||
var stats = this.counterData[ruleName];
|
||||
if (!stats || stats.packets === 0) {
|
||||
return E('span', {'style': 'color: #999; font-size: 0.9em;'}, _('no activity'));
|
||||
}
|
||||
|
||||
var totalPackets = stats.packets;
|
||||
var displayText = '';
|
||||
|
||||
// Format packet count for readability
|
||||
if (totalPackets >= 1000000) {
|
||||
displayText = (totalPackets / 1000000).toFixed(1) + 'M pkts';
|
||||
} else if (totalPackets >= 1000) {
|
||||
displayText = (totalPackets / 1000).toFixed(1) + 'K pkts';
|
||||
} else {
|
||||
displayText = totalPackets + ' pkts';
|
||||
}
|
||||
|
||||
// Add IPv4/IPv6 breakdown if both are present
|
||||
var breakdown = '';
|
||||
if (stats.ipv4_packets > 0 && stats.ipv6_packets > 0) {
|
||||
breakdown = ` (v4:${stats.ipv4_packets}, v6:${stats.ipv6_packets})`;
|
||||
}
|
||||
|
||||
return E('span', {
|
||||
'style': 'color: #0a84ff; font-size: 0.9em; font-weight: 500;',
|
||||
'title': `Total: ${totalPackets} packets, ${(stats.bytes / 1024).toFixed(1)} KB${breakdown}`
|
||||
}, displayText);
|
||||
},
|
||||
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('qosmate', _('QoSmate Rules'),
|
||||
_('Configure QoS rules for marking packets with DSCP values.'));
|
||||
|
||||
s = m.section(form.GridSection, 'rule', _('Rules'));
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s.sortable = true;
|
||||
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('mapping', _('DSCP Mapping'));
|
||||
|
||||
// Add mapping information to the description
|
||||
s.description = E('div', { 'class': 'cbi-section-descr' }, [
|
||||
E('h4', _('HFSC Mapping:')),
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '25%' }, _('High Priority [Realtime] (1:11)')),
|
||||
E('td', { 'class': 'td left' }, 'EF, CS5, CS6, CS7')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Fast Non-Realtime (1:12)')),
|
||||
E('td', { 'class': 'td left' }, 'CS4, AF41, AF42')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Normal (1:13)')),
|
||||
E('td', { 'class': 'td left' }, 'CS0')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Low Priority (1:14)')),
|
||||
E('td', { 'class': 'td left' }, 'CS2, AF11')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Bulk (1:15)')),
|
||||
E('td', { 'class': 'td left' }, 'CS1')
|
||||
])
|
||||
]),
|
||||
E('p', { 'style': 'font-size:0.9em; margin: -5px 0 15px 5px;' }, [
|
||||
E('strong', _('Hybrid Note:')), ' ',
|
||||
_('Uses Realtime (1:11) & Bulk (1:15) classes with their dscp values from HFSC. All other traffic is handled by a single CAKE class (1:13).')
|
||||
]),
|
||||
E('h4', _('CAKE Mapping (diffserv4):')),
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '25%' }, _('Voice (Highest Priority)')),
|
||||
E('td', { 'class': 'td left' }, 'CS7, CS6, EF, VA, CS5, CS4')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Video')),
|
||||
E('td', { 'class': 'td left' }, 'CS3, AF4x, AF3x, AF2x, CS2, TOS1')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Best Effort')),
|
||||
E('td', { 'class': 'td left' }, 'CS0, AF1x, TOS0')
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Bulk (Lowest Priority)')),
|
||||
E('td', { 'class': 'td left' }, 'CS1, LE')
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
o = s.taboption('general', form.Value, 'name', _('Name'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.DummyValue, 'proto', _('Protocol'));
|
||||
o.cfgvalue = function(section_id) {
|
||||
var proto = uci.get('qosmate', section_id, 'proto');
|
||||
if (Array.isArray(proto)) {
|
||||
return proto.map(function(p) {
|
||||
if (p === 'ipv6-icmp') return 'ICMPv6';
|
||||
return p.toUpperCase();
|
||||
}).join(', ');
|
||||
} else if (typeof proto === 'string') {
|
||||
if (proto === 'ipv6-icmp') return 'ICMPv6';
|
||||
return proto.toUpperCase();
|
||||
}
|
||||
return _('Any');
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.MultiValue, 'proto', _('Protocol'));
|
||||
o.value('tcp', _('TCP'));
|
||||
o.value('udp', _('UDP'));
|
||||
o.value('icmp', _('ICMP'));
|
||||
o.value('ipv6-icmp', _('ICMPv6'));
|
||||
o.rmempty = true;
|
||||
o.default = 'tcp udp';
|
||||
o.modalonly = true;
|
||||
|
||||
o.cfgvalue = function(section_id) {
|
||||
var value = uci.get('qosmate', section_id, 'proto');
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
} else if (typeof value === 'string') {
|
||||
return value.split(/\s+/);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
o.write = function(section_id, value) {
|
||||
if (value && value.length) {
|
||||
uci.set('qosmate', section_id, 'proto', value.join(' '));
|
||||
} else {
|
||||
uci.unset('qosmate', section_id, 'proto');
|
||||
}
|
||||
};
|
||||
|
||||
o.validate = function(section_id, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var valid = ['tcp', 'udp', 'icmp', 'ipv6-icmp'];
|
||||
var toValidate = Array.isArray(value) ? value : value.split(/\s+/);
|
||||
|
||||
for (var i = 0; i < toValidate.length; i++) {
|
||||
if (valid.indexOf(toValidate[i]) === -1) {
|
||||
return _('Invalid protocol: %s').format(toValidate[i]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o.remove = function(section_id) {
|
||||
uci.unset('qosmate', section_id, 'proto');
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.DynamicList, 'src_ip', _('Source IP'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = _('IP address, range, @setname or ::suffix/::mask');
|
||||
o.rmempty = true;
|
||||
o.validate = function(section_id, value) {
|
||||
return validateIPField(section_id, value);
|
||||
};
|
||||
o.write = function(section_id, formvalue) {
|
||||
var values = formvalue.map(function(v) {
|
||||
return v.replace(/^!(?!=)/, '!=');
|
||||
});
|
||||
return this.super('write', [section_id, values]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.DynamicList, 'src_port', _('Source port'));
|
||||
o.datatype = 'list(neg(portrange))';
|
||||
o.placeholder = _('any');
|
||||
o.rmempty = true;
|
||||
o.write = function(section_id, formvalue) {
|
||||
var values;
|
||||
// Handle array and string inputs
|
||||
if (Array.isArray(formvalue)) {
|
||||
values = formvalue.map(function(v) {
|
||||
// Also handle if individual values contain spaces
|
||||
if (typeof v === 'string' && v.indexOf(' ') !== -1) {
|
||||
return v.split(/\s+/).map(function(part) {
|
||||
return part.replace(/^!(?!=)/, '!=');
|
||||
}).join(' ');
|
||||
}
|
||||
return typeof v === 'string' ? v.replace(/^!(?!=)/, '!=') : v;
|
||||
});
|
||||
} else if (typeof formvalue === 'string') {
|
||||
// If it's a string, split by spaces and process each part
|
||||
values = formvalue.split(/\s+/).map(function(v) {
|
||||
return v.replace(/^!(?!=)/, '!=');
|
||||
});
|
||||
} else {
|
||||
values = formvalue;
|
||||
}
|
||||
return this.super('write', [section_id, values]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.DynamicList, 'dest_ip', _('Destination IP'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = _('IP address, range, @setname or ::suffix/::mask');
|
||||
o.rmempty = true;
|
||||
o.validate = function(section_id, value) {
|
||||
return validateIPField(section_id, value);
|
||||
};
|
||||
o.write = function(section_id, formvalue) {
|
||||
var values = formvalue.map(function(v) {
|
||||
return v.replace(/^!(?!=)/, '!=');
|
||||
});
|
||||
return this.super('write', [section_id, values]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.DynamicList, 'dest_port', _('Destination port'));
|
||||
o.datatype = 'list(neg(portrange))';
|
||||
o.placeholder = _('any');
|
||||
o.rmempty = true;
|
||||
o.write = function(section_id, formvalue) {
|
||||
var values;
|
||||
// Handle array and string inputs
|
||||
if (Array.isArray(formvalue)) {
|
||||
values = formvalue.map(function(v) {
|
||||
// Also handle if individual values contain spaces
|
||||
if (typeof v === 'string' && v.indexOf(' ') !== -1) {
|
||||
return v.split(/\s+/).map(function(part) {
|
||||
return part.replace(/^!(?!=)/, '!=');
|
||||
}).join(' ');
|
||||
}
|
||||
return typeof v === 'string' ? v.replace(/^!(?!=)/, '!=') : v;
|
||||
});
|
||||
} else if (typeof formvalue === 'string') {
|
||||
// If it's a string, split by spaces and process each part
|
||||
values = formvalue.split(/\s+/).map(function(v) {
|
||||
return v.replace(/^!(?!=)/, '!=');
|
||||
});
|
||||
} else {
|
||||
values = formvalue;
|
||||
}
|
||||
return this.super('write', [section_id, values]);
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'class', _('DSCP Class'));
|
||||
o.value('ef', _('EF - Expedited Forwarding (46)'));
|
||||
o.value('cs5', _('CS5 (40)'));
|
||||
o.value('cs6', _('CS6 (48)'));
|
||||
o.value('cs7', _('CS7 (56)'));
|
||||
o.value('cs4', _('CS4 (32)'));
|
||||
o.value('af41', _('AF41 (34)'));
|
||||
o.value('af42', _('AF42 (36)'));
|
||||
o.value('af11', _('AF11 (10)'));
|
||||
o.value('cs2', _('CS2 (16)'));
|
||||
o.value('cs1', _('CS1 (8)'));
|
||||
o.value('cs0', _('CS0 - Best Effort (0)'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'counter', _('Enable counter'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'trace', _('Enable trace'));
|
||||
o.rmempty = false;
|
||||
o.default = '0'; // Default to disabled
|
||||
o.description = _('Debug only');
|
||||
|
||||
o = s.taboption('general', form.Flag, 'enabled', _('Enable'));
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
o.default = '1'; // Set default value to enabled
|
||||
o.write = function(section_id, formvalue) {
|
||||
// Always write the value, whether it's '0' or '1'
|
||||
uci.set('qosmate', section_id, 'enabled', formvalue);
|
||||
};
|
||||
o.load = function(section_id) {
|
||||
var value = uci.get('qosmate', section_id, 'enabled');
|
||||
// If the value is undefined (not set in config), return '1' (enabled)
|
||||
return (value === undefined) ? '1' : value;
|
||||
};
|
||||
|
||||
// Add counter activity column for live monitoring
|
||||
o = s.option(form.DummyValue, 'counter_activity', _('Activity'));
|
||||
o.cfgvalue = function(section_id) {
|
||||
var counterEnabled = uci.get('qosmate', section_id, 'counter');
|
||||
if (counterEnabled !== '1') {
|
||||
return '-';
|
||||
}
|
||||
return 'loading...';
|
||||
};
|
||||
o.textvalue = function(section_id) {
|
||||
var counterEnabled = uci.get('qosmate', section_id, 'counter');
|
||||
if (counterEnabled !== '1') return '-';
|
||||
return 'loading...';
|
||||
};
|
||||
|
||||
var self = this;
|
||||
// Store reference to the map for grid updates
|
||||
this.gridMap = m;
|
||||
this.gridSection = s;
|
||||
|
||||
return m.render().then(function(rendered) {
|
||||
// Store view instance globally for textvalue access
|
||||
document.qosmateRulesView = self;
|
||||
|
||||
// Start counter polling
|
||||
if (!self.pollHandler) {
|
||||
self.startCounterPolling();
|
||||
}
|
||||
|
||||
return rendered;
|
||||
});
|
||||
},
|
||||
|
||||
// Handle view destruction - cleanup polling
|
||||
handleDestroy: function() {
|
||||
this.stopCounterPolling();
|
||||
},
|
||||
|
||||
// Handle window unload - cleanup polling
|
||||
handleUnload: function() {
|
||||
this.stopCounterPolling();
|
||||
}
|
||||
});
|
||||
@@ -1,991 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require tools.widgets as widgets';
|
||||
const UI_VERSION = '1.2.0';
|
||||
const UI_UPD_CHANNEL = 'release';
|
||||
|
||||
var callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
function createStatusText(status, text) {
|
||||
var colors = {
|
||||
'current': '#4CAF50', // Green
|
||||
'update': '#FF5252', // Red
|
||||
'error': '#FFC107', // Yellow
|
||||
'unknown': '#9E9E9E' // Gray
|
||||
};
|
||||
|
||||
var icons = {
|
||||
'current': '✓ ',
|
||||
'update': '↑ ',
|
||||
'error': '⚠ ',
|
||||
'unknown': '? '
|
||||
};
|
||||
|
||||
return E('span', {
|
||||
'style': 'color: ' + colors[status] + '; font-weight: bold; font-size: 13px;'
|
||||
}, icons[status] + text);
|
||||
}
|
||||
|
||||
var healthCheckData = null;
|
||||
var activeCakeType = null;
|
||||
var versionInfo = {
|
||||
backend: { current: 'Unknown', latest: 'Unknown', channel: 'Unknown' },
|
||||
frontend: { current: 'Unknown', latest: 'Unknown', channel: 'Unknown' }
|
||||
};
|
||||
|
||||
// Check if Software Flow Offloading is enabled
|
||||
function isSfoEnabled() {
|
||||
return uci.get('firewall', '@defaults[0]', 'flow_offloading') === '1';
|
||||
}
|
||||
|
||||
// The auto-setup runs detached, because a speed test regularly takes longer than uhttpd's
|
||||
// script_timeout (60s by default) and a request waiting for the whole run gets killed. Progress is
|
||||
// followed in the output file, which the backend ends with the AUTO_SETUP_DONE marker.
|
||||
function pollAutoSetupOutput(outputFile, tries, lastError) {
|
||||
// read_direct, because ubus file.read stops at the first NUL byte and the output file can
|
||||
// contain NUL padding while it is still being written
|
||||
return fs.read_direct(outputFile).catch(function(err) { lastError = err; return ''; }).then(function(output) {
|
||||
if (/^AUTO_SETUP_DONE/m.test(output))
|
||||
return output;
|
||||
|
||||
if (tries <= 0)
|
||||
return Promise.reject(new Error(lastError
|
||||
? _('Auto setup did not finish, last error: ') + lastError
|
||||
: _('Auto setup did not finish within 5 minutes.')));
|
||||
|
||||
var status = document.getElementById('qosmate-auto-setup-status');
|
||||
var lines = output.replace(/\r/g, '\n').trim().split('\n');
|
||||
if (status && lines[lines.length - 1])
|
||||
status.textContent = lines[lines.length - 1];
|
||||
|
||||
return new Promise(function(resolve) { window.setTimeout(resolve, 2000); })
|
||||
.then(function() { return pollAutoSetupOutput(outputFile, tries - 1, lastError); });
|
||||
});
|
||||
}
|
||||
|
||||
function fetchVersionInfo() {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['check_version'])
|
||||
.then(function(output) {
|
||||
// Check for API limit error
|
||||
if (output.includes('HTTP error 403') || output.includes('API rate limit exceeded')) {
|
||||
console.warn('GitHub API rate limit likely reached.');
|
||||
|
||||
// Try to parse current versions even if latest check failed due to rate limit
|
||||
const backendCurrentMatch = output.match(/Backend versions:[\s\S]*?Current version: (.+)/);
|
||||
const backendChannelMatch = output.match(/Backend versions:[\s\S]*?Update channel: (.+)/);
|
||||
const frontendCurrentMatch = output.match(/Frontend versions:[\s\S]*?Current version: (.+)/);
|
||||
const frontendChannelMatch = output.match(/Frontend versions:[\s\S]*?Update channel: (.+)/);
|
||||
|
||||
versionInfo = {
|
||||
backend: {
|
||||
current: backendCurrentMatch ? backendCurrentMatch[1].trim() : 'Unknown',
|
||||
latest: 'API limit reached',
|
||||
channel: backendChannelMatch ? backendChannelMatch[1].trim() : 'Unknown'
|
||||
},
|
||||
frontend: {
|
||||
current: frontendCurrentMatch ? frontendCurrentMatch[1].trim() : 'Unknown',
|
||||
latest: 'API limit reached',
|
||||
channel: frontendChannelMatch ? frontendChannelMatch[1].trim() : 'Unknown'
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
// Normal processing
|
||||
const backendCurrentMatch = output.match(/Backend versions:[\s\S]*?Current version: (.+)/);
|
||||
const backendLatestMatch = output.match(/Backend versions:[\s\S]*?Latest version: (.+)/);
|
||||
const backendChannelMatch = output.match(/Backend versions:[\s\S]*?Update channel: (.+)/);
|
||||
|
||||
const frontendCurrentMatch = output.match(/Frontend versions:[\s\S]*?Current version: (.+)/);
|
||||
const frontendLatestMatch = output.match(/Frontend versions:[\s\S]*?Latest version: (.+)/);
|
||||
const frontendChannelMatch = output.match(/Frontend versions:[\s\S]*?Update channel: (.+)/);
|
||||
|
||||
versionInfo = {
|
||||
backend: {
|
||||
current: backendCurrentMatch ? backendCurrentMatch[1].trim() : 'Unknown',
|
||||
latest: backendLatestMatch ? backendLatestMatch[1].trim() : 'Unknown',
|
||||
channel: backendChannelMatch ? backendChannelMatch[1].trim() : 'Unknown'
|
||||
},
|
||||
frontend: {
|
||||
current: frontendCurrentMatch ? frontendCurrentMatch[1].trim() : 'Unknown',
|
||||
latest: frontendLatestMatch ? frontendLatestMatch[1].trim() : 'Unknown',
|
||||
channel: frontendChannelMatch ? frontendChannelMatch[1].trim() : 'Unknown'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return versionInfo;
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('Error fetching version information:', error);
|
||||
// Keep previous info if available, otherwise set to error state
|
||||
if (versionInfo.backend.current === 'Unknown' && versionInfo.frontend.current === 'Unknown') {
|
||||
versionInfo = {
|
||||
backend: { current: 'Unknown', latest: 'Error', channel: 'Unknown' },
|
||||
frontend: { current: 'Unknown', latest: 'Error', channel: 'Unknown' }
|
||||
};
|
||||
} else {
|
||||
versionInfo.backend.latest = 'Error';
|
||||
versionInfo.frontend.latest = 'Error';
|
||||
}
|
||||
|
||||
return versionInfo;
|
||||
});
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev) {
|
||||
return this.handleSave(ev)
|
||||
.then(() => ui.changes.apply())
|
||||
.then(() => uci.load('qosmate'))
|
||||
.then(() => uci.get_first('qosmate', 'global', 'enabled'))
|
||||
.then(enabled => {
|
||||
if (enabled === '0') {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(err => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
|
||||
});
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('qosmate'),
|
||||
uci.load('firewall'),
|
||||
this.fetchHealthCheck(),
|
||||
fetchVersionInfo(),
|
||||
fs.read('/tmp/qosmate/cake_type').then(function(res) {
|
||||
activeCakeType = (res || '').trim() || null;
|
||||
}).catch(function() { activeCakeType = null; })
|
||||
]).catch(error => {
|
||||
console.error('Error in load function:', error);
|
||||
ui.addNotification(null, E('p', _('Error loading initial data: %s').format(error.message || error)), 'error');
|
||||
return [null, null, null];
|
||||
});
|
||||
},
|
||||
|
||||
fetchHealthCheck: function() {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['health_check'])
|
||||
.then((res) => {
|
||||
var output = res.trim();
|
||||
// Parse the full status string (everything between status= and ;errors=)
|
||||
var statusMatch = output.match(/status=(.*?);errors=/);
|
||||
var errorsMatch = output.match(/errors=(\d+)$/);
|
||||
|
||||
var statusString = statusMatch ? statusMatch[1] : 'Unknown';
|
||||
var errorsCount = errorsMatch ? parseInt(errorsMatch[1]) : 0;
|
||||
|
||||
var statusSegments = statusString.split(';');
|
||||
var detailsArray = [];
|
||||
statusSegments.forEach(function(segment) {
|
||||
if (!segment) return;
|
||||
detailsArray.push(segment);
|
||||
});
|
||||
|
||||
healthCheckData = {
|
||||
details: detailsArray,
|
||||
errors: errorsCount
|
||||
};
|
||||
// console.log("Health check data loaded successfully:", healthCheckData);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Health check failed:', err);
|
||||
healthCheckData = {
|
||||
details: ['Health check failed: ' + err],
|
||||
errors: 1
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s_info, s_status, o;
|
||||
|
||||
m = new form.Map('qosmate', _(''),
|
||||
_('For detailed setup instructions and advanced configuration options, please check the ') +
|
||||
'<a href="https://github.com/hudra0/qosmate/blob/main/README.md" target="_blank" style="color: #1976d2; text-decoration: none;">README</a>.');
|
||||
|
||||
s_info = m.section(form.NamedSection, 'global', 'global', _('Version & Updates'));
|
||||
s_info.anonymous = true;
|
||||
|
||||
// Version information
|
||||
o = s_info.option(form.DummyValue, '_version', _('Version Information'));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
// Determine if an update is available for backend
|
||||
var backendUpdateAvailable = versionInfo.backend.current !== versionInfo.backend.latest &&
|
||||
versionInfo.backend.current !== 'Unknown' &&
|
||||
versionInfo.backend.latest !== 'Unknown' &&
|
||||
versionInfo.backend.latest !== 'API limit reached' &&
|
||||
versionInfo.backend.latest !== 'Error';
|
||||
|
||||
// Determine if an update is available for frontend
|
||||
var frontendUpdateAvailable = versionInfo.frontend.current !== versionInfo.frontend.latest &&
|
||||
versionInfo.frontend.current !== 'Unknown' &&
|
||||
versionInfo.frontend.latest !== 'Unknown' &&
|
||||
versionInfo.frontend.latest !== 'API limit reached' &&
|
||||
versionInfo.frontend.latest !== 'Error';
|
||||
|
||||
var container = E('div');
|
||||
|
||||
// Create each component section
|
||||
function createComponentSection(title, info) {
|
||||
var section = E('div', { 'style': 'display: flex; align-items: center; margin-bottom: 8px;' });
|
||||
var titleEl = E('div', {
|
||||
'style': 'display: inline-block; min-width: 75px; font-weight: bold; margin-right: 1px;'
|
||||
}, title);
|
||||
|
||||
section.appendChild(titleEl);
|
||||
|
||||
var channelEl = E('div', {
|
||||
'style': 'display: inline-block; min-width: 70px; color: #888; margin-right: 15px;'
|
||||
}, [
|
||||
E('span', { 'style': 'font-family: monospace;' }, 'Channel: ' + info.channel)
|
||||
]);
|
||||
|
||||
section.appendChild(channelEl);
|
||||
|
||||
function isLikelyCommitHash(s) {
|
||||
// Check if string is a hex string longer than 10 chars.
|
||||
return typeof s === 'string' && s.length > 10 && /^[a-fA-F0-9]+$/.test(s);
|
||||
}
|
||||
|
||||
var currentVersionText = isLikelyCommitHash(info.current) ? info.current.substring(0, 7) : info.current;
|
||||
var latestVersionText = isLikelyCommitHash(info.latest) ? info.latest.substring(0, 7) : info.latest;
|
||||
|
||||
var versionInfo = E('div', { 'style': 'display: inline-block; margin-right: 10px;' }, [
|
||||
currentVersionText,
|
||||
' → ',
|
||||
E('span', {
|
||||
'style': (title === 'Backend' && backendUpdateAvailable) ||
|
||||
(title === 'Frontend' && frontendUpdateAvailable) ?
|
||||
'color: #ff7d7d; font-weight: bold;' : ''
|
||||
}, latestVersionText)
|
||||
]);
|
||||
|
||||
section.appendChild(versionInfo);
|
||||
|
||||
var statusType = '';
|
||||
var statusText = '';
|
||||
|
||||
if ((title === 'Backend' && backendUpdateAvailable) ||
|
||||
(title === 'Frontend' && frontendUpdateAvailable)) {
|
||||
statusType = 'update';
|
||||
statusText = _('UPDATE AVAILABLE');
|
||||
} else if (info.latest === 'API limit reached') {
|
||||
statusType = 'error';
|
||||
statusText = _('API LIMIT');
|
||||
} else if (info.latest === 'Error') {
|
||||
statusType = 'error';
|
||||
statusText = _('ERROR');
|
||||
} else if (info.current !== 'Unknown' && info.latest !== 'Unknown') {
|
||||
statusType = 'current';
|
||||
statusText = _('CURRENT');
|
||||
} else {
|
||||
statusType = 'unknown';
|
||||
statusText = _('UNKNOWN');
|
||||
}
|
||||
|
||||
section.appendChild(createStatusText(statusType, statusText));
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
// Create component sections
|
||||
container.appendChild(createComponentSection('Backend', versionInfo.backend));
|
||||
container.appendChild(createComponentSection('Frontend', versionInfo.frontend));
|
||||
|
||||
// Add the update button if updates are available
|
||||
if (backendUpdateAvailable || frontendUpdateAvailable) {
|
||||
var buttonContainer = E('div', { 'style': 'margin-top: 10px;' });
|
||||
|
||||
var updateButton = E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
// Create update wizard modal
|
||||
var updateOptions = [];
|
||||
|
||||
if (backendUpdateAvailable) {
|
||||
updateOptions.push({
|
||||
name: 'backend',
|
||||
title: _('Backend'),
|
||||
current: versionInfo.backend.current,
|
||||
latest: versionInfo.backend.latest
|
||||
});
|
||||
}
|
||||
|
||||
if (frontendUpdateAvailable) {
|
||||
updateOptions.push({
|
||||
name: 'frontend',
|
||||
title: _('Frontend'),
|
||||
current: versionInfo.frontend.current,
|
||||
latest: versionInfo.frontend.latest
|
||||
});
|
||||
}
|
||||
|
||||
var modalContent = [
|
||||
E('h4', {}, _('Update QoSmate Components')),
|
||||
E('p', {}, _('Select components to update:'))
|
||||
];
|
||||
|
||||
var checkboxes = {};
|
||||
updateOptions.forEach(function(option) {
|
||||
var checkbox = E('input', {
|
||||
'type': 'checkbox',
|
||||
'id': 'update_' + option.name,
|
||||
'name': 'update_' + option.name,
|
||||
'checked': 'checked'
|
||||
});
|
||||
|
||||
checkboxes[option.name] = checkbox;
|
||||
|
||||
modalContent.push(
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title', 'for': 'update_' + option.name }, option.title),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
checkbox,
|
||||
' ',
|
||||
option.current,
|
||||
' → ',
|
||||
E('span', { 'style': 'color: #ff7d7d; font-weight: bold;' }, option.latest)
|
||||
])
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// Update channel info
|
||||
modalContent.push(
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Update Channel')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('span', {}, versionInfo.backend.channel === versionInfo.frontend.channel ?
|
||||
versionInfo.backend.channel :
|
||||
_('%s (Backend) / %s (Frontend)').format(versionInfo.backend.channel, versionInfo.frontend.channel)),
|
||||
E('div', { 'style': 'font-size: 90%; color: #888; margin-top: 5px;' },
|
||||
_('To change the update channel, go to the Advanced tab'))
|
||||
])
|
||||
])
|
||||
);
|
||||
|
||||
// Add buttons
|
||||
modalContent.push(
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-positive',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
var componentsToUpdate = [];
|
||||
|
||||
updateOptions.forEach(function(option) {
|
||||
if (checkboxes[option.name].checked) {
|
||||
componentsToUpdate.push(option.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (componentsToUpdate.length === 0) {
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
|
||||
ui.showModal(_('Updating QoSmate'), [
|
||||
E('p', { 'class': 'spinning' }, _('Please wait while QoSmate is being updated...'))
|
||||
]);
|
||||
|
||||
// Use the current channel from versionInfo
|
||||
var selectedChannel;
|
||||
if (versionInfo.backend.channel === versionInfo.frontend.channel) {
|
||||
selectedChannel = versionInfo.backend.channel;
|
||||
} else {
|
||||
// If channels are mixed, show a confirmation dialog
|
||||
if (!confirm(_('Components are using different update channels (%s/%s). Continue with %s channel?')
|
||||
.format(versionInfo.backend.channel, versionInfo.frontend.channel, versionInfo.backend.channel))) {
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
selectedChannel = versionInfo.backend.channel;
|
||||
}
|
||||
|
||||
var updateArgs = ['update'];
|
||||
|
||||
// Add component selection if not updating both components
|
||||
if (componentsToUpdate.length === 1) {
|
||||
updateArgs.push('-c', componentsToUpdate[0]);
|
||||
}
|
||||
|
||||
// Add channel selection
|
||||
updateArgs.push('-v', selectedChannel);
|
||||
|
||||
console.log('Executing update command:', '/etc/init.d/qosmate', updateArgs);
|
||||
|
||||
return fs.exec_direct('/etc/init.d/qosmate', updateArgs)
|
||||
.then(function(result) {
|
||||
console.log('Update command result:', result);
|
||||
// If result contains error messages, treat it as an error
|
||||
if (result && (result.includes('error') || result.includes('Error') || result.includes('failed'))) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Update process encountered issues: %s').format(result)), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate update completion after 5 seconds
|
||||
setTimeout(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('QoSmate updated successfully.')), 'success');
|
||||
window.setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}, 5000);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('Update error:', err);
|
||||
ui.hideModal();
|
||||
|
||||
var errorMessage = _('Failed to update QoSmate');
|
||||
|
||||
if (err) {
|
||||
if (typeof err === 'string') {
|
||||
errorMessage += ': ' + err;
|
||||
} else if (err.message) {
|
||||
errorMessage += ': ' + err.message;
|
||||
} else {
|
||||
errorMessage += ': ' + JSON.stringify(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedChannel === 'snapshot') {
|
||||
errorMessage += '. ' + _('Note: The "snapshot" channel might not be available in the current repository configuration.');
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', errorMessage), 'error');
|
||||
});
|
||||
})
|
||||
}, _('Update Now'))
|
||||
])
|
||||
);
|
||||
|
||||
ui.showModal(_('QoSmate Update'), modalContent);
|
||||
})
|
||||
}, [
|
||||
E('span', { 'class': 'cbi-button-icon cbi-icon-reload' }),
|
||||
' ',
|
||||
_('Update QoSmate')
|
||||
]);
|
||||
|
||||
buttonContainer.appendChild(updateButton);
|
||||
container.appendChild(buttonContainer);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Version Information')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
container
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Section, also targeting 'global' but with a UI title for grouping
|
||||
s_status = m.section(form.NamedSection, 'global', 'global', _('Service Status & Control'));
|
||||
|
||||
// Service Status (Health Check)
|
||||
o = s_status.option(form.DummyValue, '_health_check', _(''));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
if (!healthCheckData) {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Service Status')),
|
||||
E('div', { 'class': 'cbi-value-field' }, _('Loading health check status...'))
|
||||
]);
|
||||
}
|
||||
|
||||
var statusHtml = E('div', { 'class': 'health-status', 'style': 'display: flex; gap: 16px; align-items: center;' });
|
||||
|
||||
healthCheckData.details.forEach(function(detail) {
|
||||
var [type, status] = detail.split(':');
|
||||
var displayType = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
var icon, color;
|
||||
switch(status.toLowerCase()) {
|
||||
case 'enabled':
|
||||
case 'started':
|
||||
icon = '✓';
|
||||
color = 'green';
|
||||
break;
|
||||
case 'disabled':
|
||||
case 'stopped':
|
||||
icon = '✕';
|
||||
color = 'red';
|
||||
break;
|
||||
case 'ok':
|
||||
icon = '✓';
|
||||
color = 'green';
|
||||
break;
|
||||
case 'failed':
|
||||
icon = '✕';
|
||||
color = 'red';
|
||||
break;
|
||||
case 'missing':
|
||||
icon = '⚠';
|
||||
color = 'orange';
|
||||
break;
|
||||
default:
|
||||
icon = '⚠';
|
||||
color = 'orange';
|
||||
}
|
||||
|
||||
statusHtml.appendChild(
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 4px;', 'title': detail }, [
|
||||
E('span', {
|
||||
'style': 'color: ' + color + '; font-size: 15px; font-weight: bold; min-width: 20px;'
|
||||
}, icon),
|
||||
E('span', {
|
||||
'style': 'font-size: 13px; color: #666;'
|
||||
}, _(displayType)),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Service Status')),
|
||||
E('div', { 'class': 'cbi-value-field' }, statusHtml)
|
||||
]);
|
||||
};
|
||||
|
||||
// Service Control buttons
|
||||
o = s_status.option(form.DummyValue, '_buttons', _(''));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
var buttonStyle = 'button cbi-button';
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Service Control')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': buttonStyle + ' cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['start'])
|
||||
.then(function() {
|
||||
ui.addNotification(null, E('p', _('QoSmate started')), 'success');
|
||||
window.setTimeout(function() { location.reload(); }, 1000);
|
||||
})
|
||||
.catch(function(e) { ui.addNotification(null, E('p', _('Failed to start QoSmate: ') + e), 'error'); });
|
||||
})
|
||||
}, _('Start')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': buttonStyle + ' cbi-button-neutral',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart'])
|
||||
.then(function() {
|
||||
ui.addNotification(null, E('p', _('QoSmate restarted')), 'success');
|
||||
window.setTimeout(function() { location.reload(); }, 1000);
|
||||
})
|
||||
.catch(function(e) { ui.addNotification(null, E('p', _('Failed to restart QoSmate: ') + e), 'error'); });
|
||||
})
|
||||
}, _('Restart')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': buttonStyle + ' cbi-button-reset',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['stop'])
|
||||
.then(function() {
|
||||
ui.addNotification(null, E('p', _('QoSmate stopped')), 'success');
|
||||
window.setTimeout(function() { location.reload(); }, 1000);
|
||||
})
|
||||
.catch(function(e) { ui.addNotification(null, E('p', _('Failed to stop QoSmate: ') + e), 'error'); });
|
||||
})
|
||||
}, _('Stop'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Auto Setup Button
|
||||
o = s_status.option(form.Button, '_auto_setup', _('Auto Setup'));
|
||||
o.inputstyle = 'apply';
|
||||
o.inputtitle = _('Start Auto Setup');
|
||||
o.onclick = ui.createHandlerFn(this, function() {
|
||||
ui.showModal(_('Auto Setup'), [
|
||||
E('p', _('This will run a speed test and configure QoSmate automatically.')),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Gaming Device IP (optional)')),
|
||||
E('input', { 'id': 'gaming_ip', 'type': 'text', 'class': 'cbi-input-text' })
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
var gamingIp = document.getElementById('gaming_ip').value;
|
||||
ui.showModal(_('Running Auto Setup'), [
|
||||
E('p', { 'class': 'spinning', 'id': 'qosmate-auto-setup-status' }, _('Please wait while the auto setup is in progress...')),
|
||||
E('div', { 'style': 'margin-top: 1em; border-top: 1px solid #ccc; padding-top: 1em;' }, [
|
||||
E('p', { 'style': 'font-weight: bold;' }, _('Note:')),
|
||||
E('p', _('Router-based speed tests may underestimate actual speeds. These results serve as a starting point and may require manual adjustment for optimal performance.'))
|
||||
])
|
||||
]);
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['auto_setup_detached', gamingIp])
|
||||
.then(function(res) {
|
||||
var outputFile = (res || '').trim();
|
||||
|
||||
// a backend without auto_setup_detached prints its usage instead of a path
|
||||
var run = (outputFile.charAt(0) === '/')
|
||||
? pollAutoSetupOutput(outputFile, 150)
|
||||
: fs.exec_direct('/etc/init.d/qosmate', ['auto_setup_noninteractive', gamingIp])
|
||||
.then(function(res2) { return fs.read_direct((res2 || '').trim()); });
|
||||
|
||||
return run.then(function(output) {
|
||||
ui.hideModal();
|
||||
|
||||
var wanInterface = output.match(/Detected WAN interface: (.+)/);
|
||||
var downloadSpeed = output.match(/Download speed: ([\d.]+) Mbit\/s/);
|
||||
var uploadSpeed = output.match(/Upload speed: ([\d.]+) Mbit\/s/);
|
||||
var downrate = output.match(/DOWNRATE: (\d+) kbps/);
|
||||
var uprate = output.match(/UPRATE: (\d+) kbps/);
|
||||
|
||||
if (!downloadSpeed || !uploadSpeed || parseFloat(downloadSpeed[1]) <= 0 || parseFloat(uploadSpeed[1]) <= 0 ||
|
||||
!downrate || !uprate || parseInt(downrate[1]) <= 0 || parseInt(uprate[1]) <= 0) {
|
||||
ui.addNotification(null, [
|
||||
E('p', _('Invalid speed test results. Please try again or set values manually.')),
|
||||
E('pre', { 'style': 'white-space: pre-wrap; max-height: 12em; overflow: auto;' },
|
||||
output.trim().split('\n').slice(-10).join('\n'))
|
||||
], 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 'rules?' keeps this working with backends that report a single rule
|
||||
var gamingRules = output.match(/Gaming device rules? added for IP: (.+)/);
|
||||
var existingRule = output.match(/A rule for IP (.+) already exists/);
|
||||
var warnings = output.match(/^Warning: .+$/gm) || [];
|
||||
|
||||
ui.showModal(_(''), [
|
||||
E('h2', { 'style': 'text-align:center; margin-bottom: 1em;' }, _('Auto Setup Results')),
|
||||
E('h3', { 'style': 'margin-bottom: 0.5em;' }, _('Speed Test Results')),
|
||||
E('p', { 'style': 'color: orange; margin-bottom: 1em;' }, _('Note: Router-based speed tests may underestimate actual speeds. For best results, consider running tests from a LAN device and manually entering the values. These results serve as a starting point.')),
|
||||
warnings.length ? E('div', { 'class': 'alert-message warning', 'style': 'margin-bottom: 1em;' },
|
||||
warnings.map(function(line) { return E('p', {}, line.replace(/^Warning: /, '')); })) : '',
|
||||
E('div', { 'style': 'display: table; width: 100%;' }, [
|
||||
E('div', { 'style': 'display: table-row;' }, [
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('WAN Interface')),
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px;' }, wanInterface ? wanInterface[1] : _('Not detected'))
|
||||
]),
|
||||
E('div', { 'style': 'display: table-row;' }, [
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Download Speed')),
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px;' }, downloadSpeed ? downloadSpeed[1] + ' Mbit/s' : _('Not available'))
|
||||
]),
|
||||
E('div', { 'style': 'display: table-row;' }, [
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Upload Speed')),
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px;' }, uploadSpeed ? uploadSpeed[1] + ' Mbit/s' : _('Not available'))
|
||||
])
|
||||
]),
|
||||
E('h3', { 'style': 'margin-top: 1em; margin-bottom: 0.5em;' }, _('QoS Configuration')),
|
||||
E('div', { 'style': 'display: table; width: 100%;' }, [
|
||||
E('div', { 'style': 'display: table-row;' }, [
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Download Rate')),
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px;' }, downrate ? downrate[1] + ' kbps' : _('Not set'))
|
||||
]),
|
||||
E('div', { 'style': 'display: table-row;' }, [
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Upload Rate')),
|
||||
E('div', { 'style': 'display: table-cell; padding: 5px;' }, uprate ? uprate[1] + ' kbps' : _('Not set'))
|
||||
])
|
||||
]),
|
||||
(gamingRules || existingRule) ? E('div', { 'style': 'margin-top: 1em;' }, [
|
||||
E('div', { 'style': 'font-weight: bold;' }, _('Gaming Rules')),
|
||||
E('div', {}, gamingRules ? _('Added for IP: ') + gamingRules[1]
|
||||
: _('Rule already exists for IP: ') + existingRule[1])
|
||||
]) : '',
|
||||
E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
ui.showModal(_('Applying Changes'), [
|
||||
E('p', { 'class': 'spinning' }, _('Please wait while the changes are being applied...'))
|
||||
]);
|
||||
|
||||
var rootQdisc = uci.get('qosmate', 'settings', 'ROOT_QDISC');
|
||||
var downrateValue = downrate ? parseInt(downrate[1]) : 0;
|
||||
var uprateValue = uprate ? parseInt(uprate[1]) : 0;
|
||||
|
||||
if (rootQdisc === 'hfsc' && (downrateValue <= 0 || uprateValue <= 0)) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Invalid rates for HFSC. Please set non-zero values manually.')), 'error');
|
||||
} else {
|
||||
uci.set('qosmate', 'settings', 'DOWNRATE', downrateValue.toString());
|
||||
uci.set('qosmate', 'settings', 'UPRATE', uprateValue.toString());
|
||||
|
||||
uci.save()
|
||||
.then(() => {
|
||||
return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
|
||||
})
|
||||
.then(() => {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('QoSmate settings updated and service restarted.')), 'success');
|
||||
window.setTimeout(function() {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
})
|
||||
.catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to update settings or restart QoSmate: ') + err), 'error');
|
||||
});
|
||||
}
|
||||
})
|
||||
}, _('Apply and Reload'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Auto setup failed: ') + err), 'error');
|
||||
});
|
||||
})
|
||||
}, _('Start'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
let s_basic = m.section(form.NamedSection, 'settings', 'settings', _('Basic Settings'));
|
||||
s_basic.anonymous = true;
|
||||
|
||||
function createOption(name, title, description, placeholder, datatype) {
|
||||
var opt = s_basic.option(form.Value, name, title, description);
|
||||
opt.datatype = datatype || 'string';
|
||||
opt.rmempty = true;
|
||||
opt.placeholder = placeholder;
|
||||
|
||||
if (datatype === 'uinteger') {
|
||||
opt.validate = function(section_id, value) {
|
||||
if (value === '' || value === null) return true;
|
||||
if (!/^\d+$/.test(value)) return _('Must be a non-negative integer or empty');
|
||||
// 0 is valid for every qdisc: it disables shaping for that direction
|
||||
return true;
|
||||
};
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
var wanInterface = uci.get('qosmate', 'settings', 'WAN') || '';
|
||||
o = s_basic.option(widgets.DeviceSelect, 'WAN', _('WAN Interface'), _('Select the WAN interface'));
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
o.default = wanInterface;
|
||||
|
||||
createOption('DOWNRATE', _('Download Rate (kbps)'), _('Set the download rate in kbps. 0 disables ingress shaping (no IFB device is created).'), _('Default: 90000'), 'uinteger');
|
||||
createOption('UPRATE', _('Upload Rate (kbps)'), _('Set the upload rate in kbps. 0 disables egress shaping.'), _('Default: 45000'), 'uinteger');
|
||||
|
||||
// Function to get QDisc description based on value
|
||||
function getQdiscDescriptionForValue(value) {
|
||||
switch(value) {
|
||||
case 'hfsc':
|
||||
return _('HFSC - Hierarchical Fair Service Curve. Configure realtime traffic settings in the HFSC tab.');
|
||||
case 'cake': {
|
||||
var base = _('CAKE - Common Applications Kept Enhanced. Configure CAKE-specific parameters in the CAKE tab.');
|
||||
if (!activeCakeType) return base;
|
||||
var useMq = uci.get('qosmate', 'cake', 'USE_MQ') || '0';
|
||||
var mqStatus;
|
||||
if (activeCakeType === 'cake_mq') {
|
||||
mqStatus = ' ✓ ' + _('cake_mq active');
|
||||
} else if (useMq === '1') {
|
||||
mqStatus = ' ⚠ ' + _('cake_mq not available');
|
||||
} else {
|
||||
mqStatus = ' — ' + _('cake_mq off');
|
||||
}
|
||||
return base + mqStatus;
|
||||
}
|
||||
case 'hybrid':
|
||||
return _('Hybrid - HFSC as shaper, Game Qdisc for realtime traffic, CAKE for default traffic and fq_codel for bulk traffic. Configure realtime class settings in HFSC tab and default class settings in CAKE tab.');
|
||||
case 'htb':
|
||||
return _('HTB - Hierarchical Token Bucket. Simple 3-tier priority system with pre-configured settings - no additional qdisc configuration required.');
|
||||
default:
|
||||
return _('Select the root queueing discipline');
|
||||
}
|
||||
}
|
||||
|
||||
// Get description for current value
|
||||
function getQdiscDescription() {
|
||||
var rootQdisc = uci.get('qosmate', 'settings', 'ROOT_QDISC') || 'hfsc';
|
||||
return getQdiscDescriptionForValue(rootQdisc);
|
||||
}
|
||||
|
||||
o = s_basic.option(form.ListValue, 'ROOT_QDISC', _('Root Queueing Discipline'), getQdiscDescription());
|
||||
o.value('hfsc', _('HFSC'));
|
||||
o.value('cake', _('CAKE'));
|
||||
o.value('hybrid', _('Hybrid'));
|
||||
o.value('htb', _('HTB (Experimental)'));
|
||||
o.default = 'hfsc';
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
// Update description dynamically using shared function
|
||||
var newDescription = getQdiscDescriptionForValue(value);
|
||||
|
||||
// Find and update the description element
|
||||
var node = ev.target.closest('.cbi-value');
|
||||
if (node) {
|
||||
var descNode = node.querySelector('.cbi-value-description');
|
||||
if (descNode) {
|
||||
descNode.textContent = newDescription;
|
||||
}
|
||||
}
|
||||
|
||||
// Update dependent fields
|
||||
var downrate = this.map.lookupOption('DOWNRATE', section_id)[0];
|
||||
var uprate = this.map.lookupOption('UPRATE', section_id)[0];
|
||||
if (downrate && uprate) {
|
||||
downrate.map.checkDepends();
|
||||
uprate.map.checkDepends();
|
||||
}
|
||||
};
|
||||
|
||||
// Software Flow Offloading Warning
|
||||
o = s_basic.option(form.DummyValue, '_sfo_warning', _('Software Flow Offloading Status'));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
if (isSfoEnabled()) {
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, E('span', { 'style': 'color: orange; font-weight: bold;' }, '⚠')),
|
||||
E('div', { 'class': 'cbi-value-field', 'style': 'color: orange;' }, [
|
||||
E('strong', {}, _('Software Flow Offloading active - some limitations apply')),
|
||||
E('br'),
|
||||
_('✓ Static rules work ✗ Dynamic rules may not work')
|
||||
])
|
||||
]);
|
||||
} else {
|
||||
return E('div');
|
||||
}
|
||||
};
|
||||
|
||||
// Notice when a direction is not shaped (rate 0, same convention as sqm-scripts)
|
||||
o = s_basic.option(form.DummyValue, '_shaping_direction', _('Shaping Direction'));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
var downrate = parseInt(uci.get('qosmate', 'settings', 'DOWNRATE') || '0');
|
||||
var uprate = parseInt(uci.get('qosmate', 'settings', 'UPRATE') || '0');
|
||||
if (downrate > 0 && uprate > 0) return E('div');
|
||||
|
||||
var lines = [];
|
||||
if (downrate === 0 && uprate === 0) {
|
||||
lines.push(E('strong', {}, _('No shaping active: both rates are 0')));
|
||||
lines.push(E('br'));
|
||||
lines.push(_('Only nftables DSCP marking is applied, no queues are created.'));
|
||||
} else if (downrate === 0) {
|
||||
lines.push(E('strong', {}, _('Download shaping disabled (DOWNRATE=0)')));
|
||||
lines.push(E('br'));
|
||||
lines.push(_('No IFB device is created. TCP bulk detection is off because its thresholds derive from the download rate, CAKE ingress options have no effect, and incoming packets carry no DSCP.'));
|
||||
lines.push(E('br'));
|
||||
lines.push(_('Set ACK_FILTER_EGRESS explicitly in the CAKE tab: "auto" cannot decide without a download rate.'));
|
||||
} else {
|
||||
lines.push(E('strong', {}, _('Upload shaping disabled (UPRATE=0)')));
|
||||
lines.push(E('br'));
|
||||
lines.push(_('No egress qdisc is created, so traffic classification only affects the download queue.'));
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, E('span', { 'style': 'color: orange; font-weight: bold;' }, '⚠')),
|
||||
E('div', { 'class': 'cbi-value-field', 'style': 'color: orange;' }, lines)
|
||||
]);
|
||||
};
|
||||
|
||||
// Warning for bandwidth ratio
|
||||
o = s_basic.option(form.DummyValue, '_ratio_warning', _('Bandwidth Ratio Warning'));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
var downrate = uci.get('qosmate', 'settings', 'DOWNRATE') || '90000';
|
||||
var uprate = uci.get('qosmate', 'settings', 'UPRATE') || '45000';
|
||||
var bwmaxratio = uci.get('qosmate', 'advanced', 'BWMAXRATIO') || '20';
|
||||
|
||||
downrate = parseInt(downrate);
|
||||
uprate = parseInt(uprate);
|
||||
bwmaxratio = parseInt(bwmaxratio);
|
||||
|
||||
if (uprate > 0 && downrate / uprate > bwmaxratio) {
|
||||
var ratio = Math.floor(downrate / uprate);
|
||||
var upload_mbps = Math.floor(uprate / 1000);
|
||||
var limited_download = Math.floor(bwmaxratio * uprate / 1000);
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, E('span', { 'style': 'color: orange; font-weight: bold;' }, '⚠️')),
|
||||
E('div', { 'class': 'cbi-value-field', 'style': 'color: orange;' }, [
|
||||
E('strong', {}, _('Large download/upload difference detected (') + ratio + ':1 ratio)'),
|
||||
E('br'),
|
||||
_('Your download speed has been limited to prevent connection issues.'),
|
||||
E('br'),
|
||||
_('Current limit: ') + limited_download + _(' Mbps (based on your ') + upload_mbps + _(' Mbps upload)') + _('To override: Advanced Settings → BWMAXRATIO'),
|
||||
])
|
||||
]);
|
||||
} else {
|
||||
return E('div');
|
||||
}
|
||||
};
|
||||
|
||||
// Autorate Section
|
||||
var s_autorate = m.section(form.NamedSection, 'autorate', 'autorate', _('Dynamic Bandwidth (Autorate)'));
|
||||
s_autorate.anonymous = true;
|
||||
|
||||
o = s_autorate.option(form.Flag, 'enabled', _('Enable Autorate'),
|
||||
_('Dynamically adjust bandwidth based on latency measurements. Fine-tune in Advanced tab.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s_autorate.option(form.DummyValue, '_autorate_status', _(''));
|
||||
o.rawhtml = true;
|
||||
o.render = function(section_id) {
|
||||
var enabled = uci.get('qosmate', 'autorate', 'enabled');
|
||||
if (enabled !== '1') {
|
||||
return E('div');
|
||||
}
|
||||
|
||||
var uprate = parseInt(uci.get('qosmate', 'settings', 'UPRATE') || '0');
|
||||
var downrate = parseInt(uci.get('qosmate', 'settings', 'DOWNRATE') || '0');
|
||||
|
||||
var minUl = parseInt(uci.get('qosmate', 'autorate', 'min_ul_rate')) || Math.floor(uprate * 25 / 100);
|
||||
var maxUl = parseInt(uci.get('qosmate', 'autorate', 'max_ul_rate')) || Math.floor(uprate * 105 / 100);
|
||||
var minDl = parseInt(uci.get('qosmate', 'autorate', 'min_dl_rate')) || Math.floor(downrate * 25 / 100);
|
||||
var maxDl = parseInt(uci.get('qosmate', 'autorate', 'max_dl_rate')) || Math.floor(downrate * 105 / 100);
|
||||
|
||||
return E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, _('Active Range')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [
|
||||
E('span', { 'style': 'color: #2196F3;' }, '↓ '),
|
||||
minDl + ' - ' + maxDl + ' kbps',
|
||||
E('span', { 'style': 'margin: 0 12px; color: #666;' }, '|'),
|
||||
E('span', { 'style': 'color: #4CAF50;' }, '↑ '),
|
||||
minUl + ' - ' + maxUl + ' kbps'
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
|
||||
function updateQosmate() {
|
||||
// Implement the update logic here
|
||||
ui.showModal(_('Updating QoSmate'), [
|
||||
E('p', { 'class': 'spinning' }, _('Updating QoSmate. Please wait...'))
|
||||
]);
|
||||
|
||||
// Simulating an update process
|
||||
setTimeout(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# QoSmate Translations
|
||||
|
||||
This directory contains translations for the QoSmate LuCI application.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
1. Copy templates/qosmate.pot to xx.po (where xx is your language code)
|
||||
2. Translate the strings in your xx.po file
|
||||
3. Submit a pull request
|
||||
|
||||
## Available Languages
|
||||
- English (default)
|
||||
- German (de.po) (test)
|
||||
|
||||
## Creating/Updating Translations
|
||||
Use these commands to update translations:
|
||||
|
||||
```bash
|
||||
# Update .pot template
|
||||
./scripts/i18n-scan.pl htdocs > po/templates/qosmate.pot
|
||||
|
||||
# Update existing .po files
|
||||
./scripts/i18n-update.pl po/templates/qosmate.pot po/*.po
|
||||
@@ -1,21 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: QoSmate\n"
|
||||
"PO-Revision-Date: 2024-01-09\n"
|
||||
"Last-Translator: Your Name\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: htdocs/luci-static/resources/view/qosmate/settings.js:25
|
||||
msgid "QoSmate Settings"
|
||||
msgstr "QoSmate Einstellungen"
|
||||
|
||||
#: htdocs/luci-static/resources/view/qosmate/settings.js:26
|
||||
msgid "Configure QoSmate settings."
|
||||
msgstr "Konfigurieren Sie die QoSmate-Einstellungen."
|
||||
|
||||
#: htdocs/luci-static/resources/view/qosmate/hfsc.js:15
|
||||
msgid "HFSC Settings"
|
||||
msgstr "HFSC-Einstellungen"
|
||||
@@ -1,14 +0,0 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: htdocs/luci-static/resources/view/qosmate/settings.js:25
|
||||
msgid "QoSmate Settings"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/qosmate/settings.js:26
|
||||
msgid "Configure QoSmate settings."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/qosmate/hfsc.js:15
|
||||
msgid "HFSC Settings"
|
||||
msgstr ""
|
||||
@@ -1 +0,0 @@
|
||||
zh_Hans
|
||||
@@ -1,173 +0,0 @@
|
||||
#!/usr/bin/env lua
|
||||
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
-- Check if an IP address is private - not needed at the moment
|
||||
local function is_private_ip(ip)
|
||||
if not ip or ip == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if ip:match(":") then -- IPv6
|
||||
-- Check for ULA (fc00::/7), link-local (fe80::/10), or loopback (::1)
|
||||
return ip:match("^f[cd]") or ip:match("^fe80:") or ip == "::1"
|
||||
else -- IPv4
|
||||
local octets = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
|
||||
if not octets[1] then return false end
|
||||
local first, second = tonumber(octets[1]), tonumber(octets[2])
|
||||
-- Check for private IPv4 ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
||||
return (first == 10) or
|
||||
(first == 172 and second >= 16 and second <= 31) or
|
||||
(first == 192 and second == 168)
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse a single connection line from nf_conntrack
|
||||
local function parse_connection(line)
|
||||
local conn = {}
|
||||
conn.layer3, conn.layer4, conn.protocol, conn.timeout = line:match("(%w+)%s+(%d+)%s+(%w+)%s+(%d+)")
|
||||
if not (conn.layer3 and conn.layer4 and conn.protocol and conn.timeout) then
|
||||
return nil
|
||||
end
|
||||
|
||||
if conn.protocol == "icmp" or conn.protocol == "icmpv6" then
|
||||
-- Handle ICMP and ICMPv6 protocols
|
||||
conn.src, conn.dst, conn.type, conn.code, conn.id =
|
||||
line:match("src=(%S+)%s+dst=(%S+)%s+type=(%d+)%s+code=(%d+)%s+id=(%d+)")
|
||||
conn.in_packets, conn.in_bytes = line:match("packets=(%d+)%s+bytes=(%d+)")
|
||||
conn.out_packets, conn.out_bytes = conn.in_packets, conn.in_bytes
|
||||
conn.sport, conn.dport = "-", "-" -- ICMP doesn't use ports
|
||||
else
|
||||
-- Handle other protocols (e.g., TCP, UDP)
|
||||
local src1, dst1, sport1, dport1, packets1, bytes1,
|
||||
src2, dst2, sport2, dport2, packets2, bytes2 =
|
||||
line:match("src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+).*src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+)")
|
||||
|
||||
-- Always use the first direction as the original
|
||||
conn.src, conn.dst, conn.sport, conn.dport = src1, dst1, sport1, dport1
|
||||
conn.out_packets, conn.in_packets = tonumber(packets1), tonumber(packets2)
|
||||
conn.out_bytes, conn.in_bytes = tonumber(bytes1), tonumber(bytes2)
|
||||
end
|
||||
|
||||
if not (conn.src and conn.dst) then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Extract additional connection information
|
||||
conn.dscp = math.floor(tonumber(line:match("mark=(%d+)") or "0"))
|
||||
conn.use = tonumber(line:match("use=(%d+)") or "0")
|
||||
conn.timeout = tonumber(conn.timeout) or 0
|
||||
|
||||
-- Determine connection state
|
||||
if line:match("%[ASSURED%]") then
|
||||
conn.state = "ASSURED"
|
||||
elseif line:match("%[UNREPLIED%]") then
|
||||
conn.state = "UNREPLIED"
|
||||
else
|
||||
conn.state = "UNKNOWN"
|
||||
end
|
||||
|
||||
return conn
|
||||
end
|
||||
|
||||
-- Retrieve all current connections from nf_conntrack
|
||||
local function get_connections()
|
||||
local conn_list = {}
|
||||
|
||||
-- Read MAX_CONNECTIONS from config
|
||||
local max_connections = 0
|
||||
local config = io.popen("uci get qosmate.advanced.MAX_CONNECTIONS 2>/dev/null")
|
||||
if config then
|
||||
local value = config:read("*line")
|
||||
config:close()
|
||||
if value and value ~= "" then
|
||||
max_connections = tonumber(value) or 0
|
||||
end
|
||||
end
|
||||
|
||||
local f = io.open("/proc/net/nf_conntrack", "r")
|
||||
if not f then
|
||||
return {}, max_connections
|
||||
end
|
||||
|
||||
local count = 0
|
||||
|
||||
-- Read connections from nf_conntrack (with early exit if limited)
|
||||
for line in f:lines() do
|
||||
-- Early exit if we have a limit and reached it
|
||||
if max_connections > 0 and count >= max_connections then
|
||||
break
|
||||
end
|
||||
|
||||
local conn = parse_connection(line)
|
||||
if conn then
|
||||
conn.total_bytes = (conn.in_bytes or 0) + (conn.out_bytes or 0)
|
||||
table.insert(conn_list, conn)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
|
||||
-- Sort only the connections we actually loaded
|
||||
table.sort(conn_list, function(a, b)
|
||||
return a.total_bytes > b.total_bytes
|
||||
end)
|
||||
|
||||
-- Determine max_entries based on system load (only if no manual limit)
|
||||
local max_entries = #conn_list
|
||||
if max_connections == 0 then
|
||||
local load_avg = io.open("/proc/loadavg"):read("*a")
|
||||
local load1 = tonumber(load_avg:match("^(%d+%.%d+)")) or 0
|
||||
|
||||
-- Apply load-based limits only if no manual limit
|
||||
if load1 > 2.0 then
|
||||
max_entries = math.min(100, #conn_list)
|
||||
elseif load1 > 1.5 then
|
||||
max_entries = math.min(500, #conn_list)
|
||||
elseif load1 > 0.8 then
|
||||
max_entries = math.min(1000, #conn_list)
|
||||
end
|
||||
end
|
||||
|
||||
-- Final connections table
|
||||
local connections = {}
|
||||
for i = 1, max_entries do
|
||||
local conn = conn_list[i]
|
||||
if not conn then break end
|
||||
local key = string.format("%s:%s:%s:%s:%s:%s",
|
||||
conn.layer3, conn.protocol, conn.src, conn.sport, conn.dst, conn.dport)
|
||||
connections[key] = conn
|
||||
end
|
||||
|
||||
return connections, max_connections
|
||||
end
|
||||
|
||||
-- Define RPC methods
|
||||
local methods = {
|
||||
getConntrackDSCP = {
|
||||
call = function()
|
||||
local connections, max_connections = get_connections()
|
||||
return {result = jsonc.stringify({connections = connections, max_connections = max_connections})}
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
-- Handle RPC calls
|
||||
if arg[1] == "list" then
|
||||
local rv = {}
|
||||
for _, v in pairs(methods) do
|
||||
rv[_] = v.args or {}
|
||||
end
|
||||
print((jsonc.stringify(rv):gsub(":%[%]", ":{}")))
|
||||
elseif arg[1] == "call" then
|
||||
local args = jsonc.parse(io.stdin:read("*a"))
|
||||
local method = methods[arg[2]]
|
||||
if method then
|
||||
local result = method.call(args)
|
||||
print(result.result)
|
||||
os.exit(result.code or 0)
|
||||
else
|
||||
print(jsonc.stringify({error = "Method not found"}))
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
@@ -1,491 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# QoSmate Statistics Backend for LuCI
|
||||
# shellcheck disable=SC2039,SC3043,SC2155,SC3057,SC1091
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
# Set to 1 to enable debug logging, 0 to disable
|
||||
DEBUG=0
|
||||
|
||||
# Debug logging function
|
||||
debug_log() {
|
||||
if [ "$DEBUG" -eq 1 ]; then
|
||||
echo "$1" >> /tmp/qosmate_debug.log
|
||||
fi
|
||||
}
|
||||
|
||||
# Return a safe unsigned integer for JSON number fields
|
||||
json_uint_or_zero() {
|
||||
case "$1" in
|
||||
''|*[!0-9]*) printf '0' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to safely get JSON from tc command
|
||||
get_tc_json() {
|
||||
local output
|
||||
# Fix HFSC options format by completely replacing the options object
|
||||
output=$("$@" 2>/dev/null) || echo "[]"
|
||||
|
||||
# Check if output is empty
|
||||
if [ -z "$output" ]; then
|
||||
echo "[]"
|
||||
return
|
||||
fi
|
||||
|
||||
# Fix the HFSC options format by replacing the entire options object with a simple default value
|
||||
echo "$output" | sed 's/\("kind":"hfsc",[^{]*"options":\){[^}]*}/\1{"default":13}/g'
|
||||
}
|
||||
|
||||
# Function to filter statistics to only include QoSmate relevant qdiscs
|
||||
filter_qosmate_qdiscs() {
|
||||
local stats="$1"
|
||||
local wan_interface="$2"
|
||||
local ifb_interface="$3"
|
||||
|
||||
# Filter to only include qdiscs related to QoSmate (on wan_interface and ifb_interface)
|
||||
echo "$stats" | jq -c "[.[] | select(.dev == \"$wan_interface\" or .dev == \"$ifb_interface\")]" 2>/dev/null || echo "[]"
|
||||
}
|
||||
|
||||
# Function to get the statistics based on root qdisc
|
||||
get_stats() {
|
||||
# Get basic statistics for all qdiscs
|
||||
local basic_stats
|
||||
basic_stats=$(get_tc_json tc -s -j qdisc show)
|
||||
|
||||
debug_log "Basic stats: $basic_stats"
|
||||
|
||||
# Get root qdisc type
|
||||
local root_qdisc
|
||||
root_qdisc=$(uci -q get qosmate.settings.ROOT_QDISC || echo "hfsc")
|
||||
|
||||
# Get interface names
|
||||
local wan_interface
|
||||
wan_interface=$(uci -q get qosmate.settings.WAN || echo "eth1")
|
||||
|
||||
local ifb_interface
|
||||
ifb_interface="ifb-$wan_interface"
|
||||
|
||||
# Filter basic stats to only include QoSmate relevant qdiscs
|
||||
local filtered_stats
|
||||
filtered_stats=$(filter_qosmate_qdiscs "$basic_stats" "$wan_interface" "$ifb_interface")
|
||||
debug_log "Filtered stats: $filtered_stats"
|
||||
|
||||
# Process statistics based on qdisc type
|
||||
local result
|
||||
if [ "$root_qdisc" = "cake" ]; then
|
||||
# Extract CAKE statistics for both egress and ingress
|
||||
local cake_egress_stats
|
||||
cake_egress_stats=$(echo "$filtered_stats" | jq -c "[.[] | select(.kind == \"cake\" and .dev == \"$wan_interface\")]" 2>/dev/null || echo "[]")
|
||||
debug_log "Cake egress stats: $cake_egress_stats"
|
||||
|
||||
local cake_ingress_stats
|
||||
cake_ingress_stats=$(echo "$filtered_stats" | jq -c "[.[] | select(.kind == \"cake\" and .dev == \"$ifb_interface\")]" 2>/dev/null || echo "[]")
|
||||
debug_log "Cake ingress stats: $cake_ingress_stats"
|
||||
|
||||
# Get the priority queue type (diffserv3/4/8)
|
||||
local priority_queue_ingress
|
||||
priority_queue_ingress=$(uci -q get qosmate.cake.PRIORITY_QUEUE_INGRESS || echo "diffserv4")
|
||||
local priority_queue_egress
|
||||
priority_queue_egress=$(uci -q get qosmate.cake.PRIORITY_QUEUE_EGRESS || echo "diffserv4")
|
||||
|
||||
# Extract the actual cake qdisc stats for UI compatibility
|
||||
local cake_egress
|
||||
cake_egress=$(echo "$cake_egress_stats" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
|
||||
local cake_ingress
|
||||
cake_ingress=$(echo "$cake_ingress_stats" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
|
||||
|
||||
# Create final JSON output using jq to ensure proper escaping
|
||||
result=$(jq -n \
|
||||
--arg rq "$root_qdisc" \
|
||||
--arg wi "$wan_interface" \
|
||||
--arg ii "$ifb_interface" \
|
||||
--arg pqi "$priority_queue_ingress" \
|
||||
--arg pqe "$priority_queue_egress" \
|
||||
--argjson fs "$filtered_stats" \
|
||||
--argjson ce "$cake_egress" \
|
||||
--argjson ci "$cake_ingress" \
|
||||
'{
|
||||
root_qdisc: $rq,
|
||||
wan_interface: $wi,
|
||||
ifb_interface: $ii,
|
||||
priority_queue_ingress: $pqi,
|
||||
priority_queue_egress: $pqe,
|
||||
qdisc_stats: $fs,
|
||||
cake_egress: $ce,
|
||||
cake_ingress: $ci
|
||||
}' 2>/dev/null)
|
||||
elif [ "$root_qdisc" = "hfsc" ] || [ "$root_qdisc" = "hybrid" ]; then
|
||||
# Extract HFSC root qdiscs and classes (hybrid uses HFSC root)
|
||||
local hfsc_egress_qdisc
|
||||
hfsc_egress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$wan_interface" | jq -c "[.[] | select(.kind == \"hfsc\")]" 2>/dev/null || echo "[]")
|
||||
debug_log "HFSC egress qdisc: $hfsc_egress_qdisc"
|
||||
|
||||
local hfsc_egress_classes
|
||||
hfsc_egress_classes=$(get_tc_json tc -s -j class show dev "$wan_interface")
|
||||
debug_log "HFSC egress classes: $hfsc_egress_classes"
|
||||
|
||||
# Process the output to ensure it's valid JSON
|
||||
hfsc_egress_classes=$(echo "$hfsc_egress_classes" | jq -c '.' 2>/dev/null || echo "[]")
|
||||
|
||||
local hfsc_ingress_qdisc
|
||||
hfsc_ingress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface" | jq -c "[.[] | select(.kind == \"hfsc\")]" 2>/dev/null || echo "[]")
|
||||
debug_log "HFSC ingress qdisc: $hfsc_ingress_qdisc"
|
||||
|
||||
local hfsc_ingress_classes
|
||||
hfsc_ingress_classes=$(get_tc_json tc -s -j class show dev "$ifb_interface")
|
||||
debug_log "HFSC ingress classes: $hfsc_ingress_classes"
|
||||
|
||||
# Process the output to ensure it's valid JSON
|
||||
hfsc_ingress_classes=$(echo "$hfsc_ingress_classes" | jq -c '.' 2>/dev/null || echo "[]")
|
||||
|
||||
# Get leaf qdisc statistics
|
||||
local egress_leaf_qdiscs
|
||||
egress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$wan_interface")
|
||||
debug_log "Egress leaf qdiscs: $egress_leaf_qdiscs"
|
||||
|
||||
# Process the output to ensure it's valid JSON and filter non-HFSC qdiscs
|
||||
egress_leaf_qdiscs=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "hfsc" and .parent != null)]' 2>/dev/null || echo "[]")
|
||||
|
||||
local ingress_leaf_qdiscs
|
||||
ingress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface")
|
||||
debug_log "Ingress leaf qdiscs: $ingress_leaf_qdiscs"
|
||||
|
||||
# Process the output to ensure it's valid JSON and filter non-HFSC qdiscs
|
||||
ingress_leaf_qdiscs=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "hfsc" and .parent != null)]' 2>/dev/null || echo "[]")
|
||||
|
||||
# Get game qdisc type (used by both HFSC and hybrid)
|
||||
local gameqdisc
|
||||
gameqdisc=$(uci -q get qosmate.hfsc.gameqdisc || echo "pfifo")
|
||||
|
||||
# Extract the main HFSC qdisc objects for UI compatibility
|
||||
local hfsc_egress
|
||||
hfsc_egress=$(echo "$hfsc_egress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
|
||||
local hfsc_ingress
|
||||
hfsc_ingress=$(echo "$hfsc_ingress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
|
||||
|
||||
# For hybrid mode, also extract CAKE statistics from the 1:13 classes
|
||||
local hybrid_cake_egress="{}"
|
||||
local hybrid_cake_ingress="{}"
|
||||
if [ "$root_qdisc" = "hybrid" ]; then
|
||||
# Get CAKE stats from the 1:13 parent classes
|
||||
hybrid_cake_egress=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind == "cake" and (.parent == "1:13" or .parent == "1:13:"))] | .[0] // {}' 2>/dev/null || echo "{}")
|
||||
hybrid_cake_ingress=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind == "cake" and (.parent == "1:13" or .parent == "1:13:"))] | .[0] // {}' 2>/dev/null || echo "{}")
|
||||
debug_log "Hybrid CAKE egress: $hybrid_cake_egress"
|
||||
debug_log "Hybrid CAKE ingress: $hybrid_cake_ingress"
|
||||
fi
|
||||
|
||||
# Create final JSON output using jq to ensure proper escaping
|
||||
result=$(jq -n \
|
||||
--arg rq "$root_qdisc" \
|
||||
--arg wi "$wan_interface" \
|
||||
--arg ii "$ifb_interface" \
|
||||
--arg gq "$gameqdisc" \
|
||||
--argjson fs "$filtered_stats" \
|
||||
--argjson he "$hfsc_egress" \
|
||||
--argjson hi "$hfsc_ingress" \
|
||||
--argjson hec "$hfsc_egress_classes" \
|
||||
--argjson hic "$hfsc_ingress_classes" \
|
||||
--argjson elq "$egress_leaf_qdiscs" \
|
||||
--argjson ilq "$ingress_leaf_qdiscs" \
|
||||
--argjson hce "$hybrid_cake_egress" \
|
||||
--argjson hci "$hybrid_cake_ingress" \
|
||||
'{
|
||||
root_qdisc: $rq,
|
||||
wan_interface: $wi,
|
||||
ifb_interface: $ii,
|
||||
gameqdisc: $gq,
|
||||
qdisc_stats: $fs,
|
||||
hfsc_egress: $he,
|
||||
hfsc_ingress: $hi,
|
||||
hfsc_egress_classes: $hec,
|
||||
hfsc_ingress_classes: $hic,
|
||||
egress_leaf_qdiscs: $elq,
|
||||
ingress_leaf_qdiscs: $ilq,
|
||||
hybrid_cake_egress: $hce,
|
||||
hybrid_cake_ingress: $hci
|
||||
}' 2>/dev/null)
|
||||
elif [ "$root_qdisc" = "htb" ]; then
|
||||
local htb_egress_qdisc
|
||||
htb_egress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$wan_interface" | jq -c "[.[] | select(.kind == \"htb\")]" 2>/dev/null || echo "[]")
|
||||
debug_log "HTB egress qdisc: $htb_egress_qdisc"
|
||||
|
||||
local htb_egress_classes
|
||||
htb_egress_classes=$(get_tc_json tc -s -j class show dev "$wan_interface")
|
||||
debug_log "HTB egress classes: $htb_egress_classes"
|
||||
htb_egress_classes=$(echo "$htb_egress_classes" | jq -c '.' 2>/dev/null || echo "[]")
|
||||
|
||||
local htb_ingress_qdisc
|
||||
htb_ingress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface" | jq -c "[.[] | select(.kind == \"htb\")]" 2>/dev/null || echo "[]")
|
||||
debug_log "HTB ingress qdisc: $htb_ingress_qdisc"
|
||||
|
||||
local htb_ingress_classes
|
||||
htb_ingress_classes=$(get_tc_json tc -s -j class show dev "$ifb_interface")
|
||||
debug_log "HTB ingress classes: $htb_ingress_classes"
|
||||
htb_ingress_classes=$(echo "$htb_ingress_classes" | jq -c '.' 2>/dev/null || echo "[]")
|
||||
|
||||
local egress_leaf_qdiscs
|
||||
egress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$wan_interface")
|
||||
debug_log "Egress leaf qdiscs: $egress_leaf_qdiscs"
|
||||
egress_leaf_qdiscs=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "htb" and .parent != null)]' 2>/dev/null || echo "[]")
|
||||
|
||||
local ingress_leaf_qdiscs
|
||||
ingress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface")
|
||||
debug_log "Ingress leaf qdiscs: $ingress_leaf_qdiscs"
|
||||
ingress_leaf_qdiscs=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "htb" and .parent != null)]' 2>/dev/null || echo "[]")
|
||||
|
||||
local htb_egress
|
||||
htb_egress=$(echo "$htb_egress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
|
||||
local htb_ingress
|
||||
htb_ingress=$(echo "$htb_ingress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
|
||||
|
||||
result=$(jq -n \
|
||||
--arg rq "$root_qdisc" \
|
||||
--arg wi "$wan_interface" \
|
||||
--arg ii "$ifb_interface" \
|
||||
--argjson fs "$filtered_stats" \
|
||||
--argjson he "$htb_egress" \
|
||||
--argjson hi "$htb_ingress" \
|
||||
--argjson hec "$htb_egress_classes" \
|
||||
--argjson hic "$htb_ingress_classes" \
|
||||
--argjson elq "$egress_leaf_qdiscs" \
|
||||
--argjson ilq "$ingress_leaf_qdiscs" \
|
||||
'{
|
||||
root_qdisc: $rq,
|
||||
wan_interface: $wi,
|
||||
ifb_interface: $ii,
|
||||
qdisc_stats: $fs,
|
||||
htb_egress: $he,
|
||||
htb_ingress: $hi,
|
||||
htb_egress_classes: $hec,
|
||||
htb_ingress_classes: $hic,
|
||||
egress_leaf_qdiscs: $elq,
|
||||
ingress_leaf_qdiscs: $ilq
|
||||
}' 2>/dev/null)
|
||||
fi
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Function to get historical statistics
|
||||
get_historical_stats() {
|
||||
# Note: This is currently a placeholder function for future implementation
|
||||
# In the future, this could provide historical QoS statistics from a database or log files
|
||||
# Currently returns an empty JSON object as no history is being saved
|
||||
local history_file="/tmp/qosmate_stats_history.json"
|
||||
|
||||
if [ -f "$history_file" ]; then
|
||||
cat "$history_file"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get RRD data if available
|
||||
get_rrd_data() {
|
||||
# Note: This is currently a placeholder function for future implementation
|
||||
# In the future, this could provide Round Robin Database data for generating time-series charts
|
||||
# Currently only checks if the RRD directory exists but doesn't actually process any data
|
||||
# Check if RRD data is available for QoS
|
||||
if [ -d "/tmp/rrd" ]; then
|
||||
echo "{\"rrd_available\": true}"
|
||||
else
|
||||
echo "{\"rrd_available\": false}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get autorate statistics (current state + config + history)
|
||||
get_autorate_stats() {
|
||||
config_load qosmate
|
||||
|
||||
local enabled
|
||||
config_get enabled autorate enabled "0"
|
||||
|
||||
if [ "$enabled" != "1" ]; then
|
||||
echo '{"autorate_enabled":false}'
|
||||
return
|
||||
fi
|
||||
|
||||
# Read current state from daemon's state file
|
||||
local current='{"ul_rate":0,"dl_rate":0,"achieved_ul":0,"achieved_dl":0,"latency":0,"baseline":0,"ul_load_pct":0,"dl_load_pct":0}'
|
||||
if [ -f "/tmp/qosmate-autorate-state" ]; then
|
||||
local _ar_ul=0 _ar_dl=0 _ar_aul=0 _ar_adl=0 _ar_lat=0 _ar_bl=0 _ar_ulp=0 _ar_dlp=0
|
||||
local _key _val
|
||||
while IFS='=' read -r _key _val; do
|
||||
case "$_key" in
|
||||
ul_rate) _ar_ul="$_val" ;;
|
||||
dl_rate) _ar_dl="$_val" ;;
|
||||
achieved_ul) _ar_aul="$_val" ;;
|
||||
achieved_dl) _ar_adl="$_val" ;;
|
||||
latency) _ar_lat="$_val" ;;
|
||||
baseline) _ar_bl="$_val" ;;
|
||||
ul_load_pct) _ar_ulp="$_val" ;;
|
||||
dl_load_pct) _ar_dlp="$_val" ;;
|
||||
esac
|
||||
done < "/tmp/qosmate-autorate-state"
|
||||
_ar_ul=$(json_uint_or_zero "$_ar_ul")
|
||||
_ar_dl=$(json_uint_or_zero "$_ar_dl")
|
||||
_ar_aul=$(json_uint_or_zero "$_ar_aul")
|
||||
_ar_adl=$(json_uint_or_zero "$_ar_adl")
|
||||
_ar_lat=$(json_uint_or_zero "$_ar_lat")
|
||||
_ar_bl=$(json_uint_or_zero "$_ar_bl")
|
||||
_ar_ulp=$(json_uint_or_zero "$_ar_ulp")
|
||||
_ar_dlp=$(json_uint_or_zero "$_ar_dlp")
|
||||
current=$(printf '{"ul_rate":%s,"dl_rate":%s,"achieved_ul":%s,"achieved_dl":%s,"latency":%s,"baseline":%s,"ul_load_pct":%s,"dl_load_pct":%s}' \
|
||||
"$_ar_ul" "$_ar_dl" "$_ar_aul" "$_ar_adl" "$_ar_lat" "$_ar_bl" "$_ar_ulp" "$_ar_dlp")
|
||||
fi
|
||||
|
||||
# Read config values (replicate defaults from autorate daemon)
|
||||
local uprate downrate
|
||||
config_get uprate settings UPRATE "0"
|
||||
config_get downrate settings DOWNRATE "0"
|
||||
|
||||
local base_ul base_dl min_ul min_dl max_ul max_dl dec_thr inc_thr
|
||||
config_get base_ul autorate base_ul_rate ""
|
||||
config_get base_dl autorate base_dl_rate ""
|
||||
config_get min_ul autorate min_ul_rate ""
|
||||
config_get min_dl autorate min_dl_rate ""
|
||||
config_get max_ul autorate max_ul_rate ""
|
||||
config_get max_dl autorate max_dl_rate ""
|
||||
config_get dec_thr autorate latency_decrease_threshold "10"
|
||||
config_get inc_thr autorate latency_increase_threshold "5"
|
||||
|
||||
: "${base_ul:=$uprate}"
|
||||
: "${base_dl:=$downrate}"
|
||||
: "${min_ul:=$((uprate * 25 / 100))}"
|
||||
: "${min_dl:=$((downrate * 25 / 100))}"
|
||||
: "${max_ul:=$((uprate * 105 / 100))}"
|
||||
: "${max_dl:=$((downrate * 105 / 100))}"
|
||||
|
||||
# Convert thresholds to tenths of ms (matching daemon's internal format)
|
||||
dec_thr=$((dec_thr * 10))
|
||||
inc_thr=$((inc_thr * 10))
|
||||
|
||||
base_ul=$(json_uint_or_zero "$base_ul")
|
||||
base_dl=$(json_uint_or_zero "$base_dl")
|
||||
min_ul=$(json_uint_or_zero "$min_ul")
|
||||
min_dl=$(json_uint_or_zero "$min_dl")
|
||||
max_ul=$(json_uint_or_zero "$max_ul")
|
||||
max_dl=$(json_uint_or_zero "$max_dl")
|
||||
dec_thr=$(json_uint_or_zero "$dec_thr")
|
||||
inc_thr=$(json_uint_or_zero "$inc_thr")
|
||||
|
||||
local config
|
||||
config=$(printf '{"base_ul":%s,"base_dl":%s,"min_ul":%s,"min_dl":%s,"max_ul":%s,"max_dl":%s,"decrease_threshold":%s,"increase_threshold":%s}' \
|
||||
"$base_ul" "$base_dl" "$min_ul" "$min_dl" "$max_ul" "$max_dl" "$dec_thr" "$inc_thr")
|
||||
|
||||
# Convert RAM CSV history to compact JSON arrays (60 min, 2s resolution)
|
||||
# Format: [[ts,ul,dl,a_ul,a_dl,lat,bl,ulp,dlp], ...]
|
||||
local history="[]"
|
||||
if [ -f "/tmp/qosmate-autorate-history" ] && [ -s "/tmp/qosmate-autorate-history" ]; then
|
||||
history=$(awk -F',' '{
|
||||
for(i=1; i<=9; i++) {
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
|
||||
if($i !~ /^[0-9]+$/) $i=0
|
||||
}
|
||||
if(NR>1) printf ","
|
||||
printf "[%s,%s,%s,%s,%s,%s,%s,%s,%s]",$1,$2,$3,$4,$5,$6,$7,$8,$9
|
||||
}' "/tmp/qosmate-autorate-history")
|
||||
history="[${history}]"
|
||||
fi
|
||||
|
||||
# Convert Flash CSV history to JSON (30 days, 10-min bucket summaries)
|
||||
# Format: [[ts,avg_ul,avg_dl,avg_a_ul,avg_a_dl,avg_lat,max_lat,avg_bl,avg_ulp,avg_dlp], ...]
|
||||
local flash_history="[]"
|
||||
if [ -f "/etc/qosmate.d/autorate_hourly.csv" ] && [ -s "/etc/qosmate.d/autorate_hourly.csv" ]; then
|
||||
flash_history=$(awk -F',' '{
|
||||
for(i=1; i<=10; i++) {
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $i)
|
||||
if($i !~ /^[0-9]+$/) $i=0
|
||||
}
|
||||
if(NR>1) printf ","
|
||||
printf "[%s,%s,%s,%s,%s,%s,%s,%s,%s,%s]",$1,$2,$3,$4,$5,$6,$7,$8,$9,$10
|
||||
}' "/etc/qosmate.d/autorate_hourly.csv")
|
||||
flash_history="[${flash_history}]"
|
||||
fi
|
||||
|
||||
printf '{"autorate_enabled":true,"current":%s,"config":%s,"history":%s,"flash_history":%s}\n' \
|
||||
"$current" "$config" "$history" "$flash_history"
|
||||
}
|
||||
|
||||
# Function to get nftables rule counters for active rules
|
||||
get_rule_counters() {
|
||||
local nft_output rules_json
|
||||
nft_output=$(nft -j list table inet dscptag 2>/dev/null || echo '{"nftables":[]}')
|
||||
|
||||
# Parse and aggregate rule counters by name (combine IPv4/IPv6)
|
||||
rules_json=$(echo "$nft_output" | jq -c '
|
||||
[.nftables[] |
|
||||
select(.rule != null and .rule.table == "dscptag") |
|
||||
select(.rule.comment != null) |
|
||||
select(.rule.comment | startswith("ipv4_") or startswith("ipv6_")) |
|
||||
select(.rule.expr[]?.counter != null) |
|
||||
.rule |
|
||||
{
|
||||
rule_name: (if .comment | startswith("ipv4_") then .comment[5:] elif .comment | startswith("ipv6_") then .comment[5:] else .comment end),
|
||||
total_packets: (.expr[] | select(.counter != null) | .counter.packets // 0),
|
||||
total_bytes: (.expr[] | select(.counter != null) | .counter.bytes // 0),
|
||||
ipv4_packets: (if .comment | startswith("ipv4_") then (.expr[] | select(.counter != null) | .counter.packets // 0) else 0 end),
|
||||
ipv6_packets: (if .comment | startswith("ipv6_") then (.expr[] | select(.counter != null) | .counter.packets // 0) else 0 end)
|
||||
}] |
|
||||
group_by(.rule_name) |
|
||||
map({
|
||||
rule_name: .[0].rule_name,
|
||||
total_packets: (map(.total_packets) | add),
|
||||
total_bytes: (map(.total_bytes) | add),
|
||||
ipv4_packets: (map(.ipv4_packets) | add // 0),
|
||||
ipv6_packets: (map(.ipv6_packets) | add // 0)
|
||||
})
|
||||
' 2>/dev/null || echo "[]")
|
||||
|
||||
jq -n --argjson rules "$rules_json" '{rule_counters: $rules}' 2>/dev/null || echo '{"rule_counters":[]}'
|
||||
}
|
||||
|
||||
# Function to handle the call command
|
||||
handle_call() {
|
||||
local method="$1"
|
||||
|
||||
# Read and discard input JSON (unused for current methods)
|
||||
cat > /dev/null
|
||||
|
||||
# Default to getStats if no method specified
|
||||
[ -z "$method" ] && method="getStats"
|
||||
|
||||
# Process based on method
|
||||
case "$method" in
|
||||
getStats)
|
||||
get_stats
|
||||
;;
|
||||
getHistoricalStats)
|
||||
get_historical_stats
|
||||
;;
|
||||
getRrdData)
|
||||
get_rrd_data
|
||||
;;
|
||||
getRuleCounters)
|
||||
get_rule_counters
|
||||
;;
|
||||
getAutorateStats)
|
||||
get_autorate_stats
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Method not found"}'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main function to handle RPC calls
|
||||
case "$1" in
|
||||
list)
|
||||
# List available methods
|
||||
echo '{"getStats": {}, "getHistoricalStats": {}, "getRrdData": {}, "getRuleCounters": {}, "getAutorateStats": {}}'
|
||||
;;
|
||||
call)
|
||||
handle_call "$2"
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Invalid call"}'
|
||||
;;
|
||||
esac
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"admin/network/qosmate": {
|
||||
"title": "QoSmate",
|
||||
"order": 70,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-qosmate" ],
|
||||
"uci": { "qosmate": true }
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/settings": {
|
||||
"title": "Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/settings"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/hfsc": {
|
||||
"title": "HFSC",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/hfsc"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/cake": {
|
||||
"title": "CAKE",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/cake"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/advanced": {
|
||||
"title": "Advanced",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/advanced"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/rules": {
|
||||
"title": "Rules",
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/rules"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/ipsets": {
|
||||
"title": "IP Sets",
|
||||
"order": 45,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/ipsets"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/ratelimits": {
|
||||
"title": "Rate Limits",
|
||||
"order": 55,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/ratelimits"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/custom_rules": {
|
||||
"title": "Custom Rules",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/custom_rules"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/connections": {
|
||||
"title": "Connections",
|
||||
"order": 70,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/connections"
|
||||
}
|
||||
},
|
||||
"admin/network/qosmate/statistics": {
|
||||
"title": "Statistics",
|
||||
"order": 80,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "qosmate/statistics"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"luci-app-qosmate": {
|
||||
"description": "Grant access to QoSmate configuration",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.qosmate": [ "getConntrackDSCP" ],
|
||||
"luci.qosmate_stats": [ "getStats", "getHistoricalStats", "getRrdData", "getRuleCounters", "getAutorateStats" ],
|
||||
"luci": [ "getInitList", "setInitAction", "getInitActionStatus", "exec" ]
|
||||
},
|
||||
"uci": [ "qosmate" ],
|
||||
"file": {
|
||||
"/etc/qosmate.sh": [ "read" ],
|
||||
"/etc/init.d/qosmate": [ "read", "exec" ],
|
||||
"/etc/hotplug.d/iface/13-qosmateHotplug": [ "read" ],
|
||||
"/etc/config/qosmate": [ "read" ],
|
||||
"/tmp/qosmate_auto_setup_output.txt": [ "read" ],
|
||||
"/etc/qosmate.d/custom_rules.nft": [ "read" ],
|
||||
"/etc/qosmate.d/inline_dscptag.nft": [ "read" ],
|
||||
"/tmp/qosmate_custom_rules_validation.txt": [ "read" ],
|
||||
"/tmp/qosmate_stats_history.json": [ "read" ],
|
||||
"/tmp/qosmate-autorate-state": [ "read" ],
|
||||
"/tmp/qosmate-autorate-history": [ "read" ],
|
||||
"/tmp/qosmate/cake_type": [ "read" ],
|
||||
"/etc/qosmate.d/autorate_hourly.csv": [ "read" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci": [ "setInitAction", "exec" ]
|
||||
},
|
||||
"uci": [ "qosmate" ],
|
||||
"file": {
|
||||
"/etc/qosmate.sh": [ "write" ],
|
||||
"/etc/init.d/qosmate": [ "write" ],
|
||||
"/etc/hotplug.d/iface/13-qosmateHotplug": [ "write" ],
|
||||
"/etc/qosmate.d/custom_rules.nft": [ "write" ],
|
||||
"/etc/qosmate.d/inline_dscptag.nft": [ "write" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
PKG_NAME="$1"
|
||||
CURDIR="$2"
|
||||
PKG_BUILD_DIR="$3"
|
||||
|
||||
if [ -d "$CURDIR/.git" ]; then
|
||||
config="$CURDIR/.git/config"
|
||||
else
|
||||
config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")"
|
||||
fi
|
||||
[ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && {
|
||||
for d in luasrc ucode htdocs root src; do
|
||||
rm -rf "$PKG_BUILD_DIR"/$d
|
||||
done
|
||||
mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view
|
||||
touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js
|
||||
mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d
|
||||
touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json
|
||||
}
|
||||
exit 0
|
||||
@@ -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,47 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0
|
||||
#
|
||||
# Copyright (C) 2022-2025 muink <https://github.com/muink>
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_NAME:=luci-app-tinyfilemanager
|
||||
PKG_VERSION:=2.6
|
||||
PKG_RELEASE:=5
|
||||
|
||||
LUCI_TITLE:=LuCI Tiny File Manager
|
||||
LUCI_DEPENDS:=+php8 +php8-cgi +php8-fastcgi +php8-fpm +php8-mod-session +php8-mod-ctype +php8-mod-fileinfo +php8-mod-zip +php8-mod-iconv +php8-mod-mbstring +coreutils-stat +zoneinfo-asia +bash +curl +tar +inotifywait
|
||||
|
||||
LUCI_DESCRIPTION:=A Web based File Manager in PHP
|
||||
|
||||
PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
PKG_UNPACK=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR)
|
||||
|
||||
define Package/$(LUCI_NAME)/conffiles
|
||||
/etc/config/tinyfilemanager
|
||||
endef
|
||||
|
||||
define Package/$(LUCI_NAME)/postinst
|
||||
#!/bin/sh
|
||||
mkdir -p "$${IPKG_INSTROOT}/www/tinyfilemanager" 2>/dev/null
|
||||
[ ! -d "$${IPKG_INSTROOT}/www/tinyfilemanager/rootfs" ] && ln -s / "$${IPKG_INSTROOT}/www/tinyfilemanager/rootfs"
|
||||
memory_limit=16M #memory_limit = 8M
|
||||
total_size_limit=5G #post_max_size = 8M
|
||||
single_size_limit=2G #upload_max_filesize = 2M
|
||||
otime_uploads_limit=200 #max_file_uploads = 20
|
||||
sed -Ei "s|^(memory_limit) *=.*$$|\1 = $$memory_limit|; \
|
||||
s|^(post_max_size) *=.*$$|\1 = $$total_size_limit|; \
|
||||
s|^(upload_max_filesize) *=.*$$|\1 = $$single_size_limit|; \
|
||||
s|^(max_file_uploads) *=.*$$|\1 = $$otime_uploads_limit|" \
|
||||
"$${IPKG_INSTROOT}/etc/php.ini"
|
||||
endef
|
||||
|
||||
define Package/$(LUCI_NAME)/prerm
|
||||
#!/bin/sh
|
||||
if [ -d /www/tinyfilemanager ]; then rm -rf /www/tinyfilemanager; fi
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -1,92 +0,0 @@
|
||||
# LuCI Tiny File Manager
|
||||
> [TinyFileManager][] is web based file manager and it is a simple, fast and small file manager with a single file, multi-language ready web application for storing, uploading, editing and managing files and folders online via web browser. The Application runs on PHP 5.5+, It allows the creation of multiple users and each user can have its own directory and a build-in support for managing text files with cloud9 IDE and it supports syntax highlighting for over 150+ languages and over 35+ themes.
|
||||
|
||||
### Screenshots
|
||||
|
||||

|
||||
|
||||
<details><summary>Real installation</summary>
|
||||
<img src="example/root.png"/>
|
||||
<img src="example/localfeeds.png"/>
|
||||
<img src="example/pictute.png"/>
|
||||
<img src="example/video.png"/>
|
||||
</details>
|
||||
|
||||
### How to install
|
||||
|
||||
1. Goto ~~[releases](https://github.com/muink/luci-app-tinyfilemanager/tree/releases)~~ [here](https://fantastic-packages.github.io/releases/)
|
||||
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 **NAS --> Tiny File Manager**
|
||||
7. Default username/password: admin/admin and user/12345.
|
||||
|
||||
### Uploading limit
|
||||
|
||||
**If you need to change the upload limit for Tiny File Manager**
|
||||
|
||||
Edit [config.js](htdocs/luci-static/resources/view/tinyfilemanager/config.js) before build\
|
||||
Edit `/www/luci-static/resources/view/tinyfilemanager/config.js` in router
|
||||
|
||||
```javascript
|
||||
o = s.option(form.Value, 'max_upload_size', _('Max upload size (MBytes)'));
|
||||
o.datatype = "and(uinteger,max(2048))"; //limit to 2048MB
|
||||
```
|
||||
|
||||
And edit [Makefile](Makefile) before build
|
||||
|
||||
```makefile
|
||||
memory_limit=?? #Memory limit
|
||||
total_size_limit=?? #Total size of multiple files
|
||||
single_size_limit=?? #Max single file size
|
||||
otime_uploads_limit=?? #Max count of simultaneous uploads
|
||||
```
|
||||
|
||||
And edit `/etc/php.ini` in router
|
||||
|
||||
```ini
|
||||
memory_limit = ?? ;Memory limit
|
||||
post_max_size = ?? ;Total size of multiple files
|
||||
upload_max_filesize = ?? ;Max single file size
|
||||
max_file_uploads = ?? ;Max count of simultaneous uploads
|
||||
```
|
||||
|
||||
### 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-tinyfilemanager.git package/luci-app-tinyfilemanager
|
||||
pushd package/luci-app-tinyfilemanager
|
||||
umask 022
|
||||
git checkout
|
||||
popd
|
||||
# Select the package LuCI -> Applications -> luci-app-tinyfilemanager
|
||||
make menuconfig
|
||||
# Upgrade to new version Tiny File Manager (optional)
|
||||
1. modify the tag VERSION='2.4.7' to new version in makenew.sh
|
||||
2. run makenew.sh to upgrade current version (if it worked)
|
||||
# Start compiling
|
||||
make package/luci-app-tinyfilemanager/compile V=99
|
||||
```
|
||||
|
||||
### Contributors
|
||||
|
||||
- [prasathmani](https://tinyfilemanager.github.io)
|
||||
- [muink](https://github.com/muink)
|
||||
|
||||
[TinyFileManager]: https://github.com/prasathmani/tinyfilemanager
|
||||
|
||||
### License
|
||||
|
||||
- This project is licensed under the [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,207 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require form';
|
||||
|
||||
return view.extend({
|
||||
// handleSaveApply: null,
|
||||
// handleSave: null,
|
||||
// handleReset: null,
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.read('/var/tinyfilemanager/releaseslist'), null),
|
||||
L.resolveDefault(fs.stat('/usr/libexec/tinyfilemanager-update'), {}),
|
||||
L.resolveDefault(fs.stat('/etc/nginx/conf.d/php.locations'), {}),
|
||||
uci.load('tinyfilemanager'),
|
||||
]);
|
||||
},
|
||||
|
||||
render(res) {
|
||||
const releaseslist = res[0] ? res[0].trim().split("\n") : [];
|
||||
const has_location = res[2].path;
|
||||
const pkgversion = '2.6';
|
||||
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('tinyfilemanager');
|
||||
|
||||
s = m.section(form.TypedSection, 'main');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Button, '_reload', _('Reload') + ' tinyfilemanager');
|
||||
o.inputtitle = _('Reload');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = function() {
|
||||
return fs.exec('/etc/init.d/tinyfilemanager', ['reload'])
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') });
|
||||
};
|
||||
|
||||
o = s.option(form.Flag, 'use_auth', _('Enable Authentication'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.DynamicList, 'auth_users', _('Login user name and passwd hash'),
|
||||
_('You can generate new passwd in <b>File Manager -> Admin -> Help -> Generate new</b> or <a href="%s" target="_blank"><b>Here</b></a>.').format('https://tinyfilemanager.github.io/docs/pwd.html'));
|
||||
|
||||
o.datatype = "list(string)";
|
||||
o.placeholder = 'user:$2y$10$cFk8K5VQJr...';
|
||||
o.default = 'admin:$2y$10$BewzfQXrlnUihprEgGt7ROMB9NigZcZkkwssIRYznF9fwMuObIZoa';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('use_auth', '1');
|
||||
|
||||
o = s.option(form.DynamicList, 'readonly_users', _('Readonly users'));
|
||||
o.datatype = "list(string)";
|
||||
o.placeholder = 'user';
|
||||
o.default = 'user';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('use_auth', '1');
|
||||
|
||||
o = s.option(form.ListValue, 'autologin_user', _('Auto login user'),
|
||||
_('Automatically login/logout of the specified user based on LuCI credentials.'));
|
||||
o.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
const users = uci.get(this.config, section_id, 'auth_users');
|
||||
this.value('', _('none'));
|
||||
for (const userpw of users)
|
||||
this.value(userpw.split(':')[0]);
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
o.depends('use_auth', '1');
|
||||
|
||||
o = s.option(form.Flag, 'global_readonly', _('Global Readonly'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'root_path', _('Home path'));
|
||||
o.datatype = 'directory';
|
||||
o.placeholder = '/var';
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.ListValue, 'date_format', _('Date format'));
|
||||
o.value('d.m.o', _('DD.MM.YYYY'));
|
||||
o.value('d-m-o', _('DD-MM-YYYY'));
|
||||
o.value('d/m/o', _('DD/MM/YYYY'));
|
||||
o.value('j.n.o', _('D.M.YYYY'));
|
||||
o.value('j-n-o', _('D-M-YYYY'));
|
||||
o.value('j/n/o', _('D/M/YYYY'));
|
||||
o.value('o.m.d', _('YYYY.MM.DD'));
|
||||
o.value('o-m-d', _('YYYY-MM-DD'));
|
||||
o.value('o/m/d', _('YYYY/MM/DD'));
|
||||
o.value('o.n.j', _('YYYY.M.D'));
|
||||
o.value('o-n-j', _('YYYY-M-D'));
|
||||
o.value('o/n/j', _('YYYY/M/D'));
|
||||
o.default = 'd.m.o';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'time_format', _('Time format'));
|
||||
o.value('H:i:s', _('HH:mm:ss'));
|
||||
o.value('G:i:s', _('H:mm:ss'));
|
||||
o.value('A h:i:s', _('TT hh:mm:ss'));
|
||||
o.value('A g:i:s', _('TT h:mm:ss'));
|
||||
o.value('h:i:s A', _('hh:mm:ss TT'));
|
||||
o.value('g:i:s A', _('h:mm:ss TT'));
|
||||
o.default = 'H:i:s';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'show_second', _('Show seconds in time'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'favicon_path', _('Favicon path'));
|
||||
o.placeholder = '/tinyfilemanager/rootfs/etc/tinyfilemanager/favicon.png';
|
||||
|
||||
o = s.option(form.DynamicList, 'exclude_items', _('Exclude Files/Folders'));
|
||||
o.datatype = "list(string)";
|
||||
o.optional = true;
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
|
||||
o = s.option(form.ListValue, 'online_viewer', _('Online Docs viewer'),
|
||||
_('Requires running on open network'));
|
||||
o.value('0', _('Disable'));
|
||||
o.value('google', _('Google Docs'));
|
||||
o.value('microsoft', _('Microsoft Web Apps'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'max_upload_size', _('Max upload size (MBytes)'));
|
||||
o.datatype = "and(uinteger,max(2048))";
|
||||
o.placeholder = '3';
|
||||
o.default = '25';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'proxy_enabled', _('Enable proxy for updater'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy Protocol'));
|
||||
o.value('http', 'HTTP');
|
||||
o.value('https', 'HTTPS');
|
||||
o.value('socks5', 'SOCKS5');
|
||||
o.value('socks5h', 'SOCKS5H');
|
||||
o.default = 'socks5';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('proxy_enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'proxy_server', _('Proxy Server'));
|
||||
o.datatype = "ipaddrport(1)";
|
||||
o.placeholder = '192.168.1.10:1080';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
o.depends('proxy_enabled', '1');
|
||||
|
||||
o = s.option(form.Button, '_check_update', _('Check update'));
|
||||
o.inputtitle = _('Check update');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = function() {
|
||||
return fs.exec('/etc/init.d/tinyfilemanager', ['check'])
|
||||
.then((res) => { return window.location.reload() })
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') });
|
||||
};
|
||||
|
||||
if (releaseslist.length) {
|
||||
o = s.option(form.ListValue, '_releaseslist', _('Releases list'));
|
||||
//o.value(pkgversion);
|
||||
o.default = pkgversion;
|
||||
for (let i = 0; i < releaseslist.length; i++)
|
||||
o.value(releaseslist[i]);
|
||||
o.write = function() {};
|
||||
|
||||
o = s.option(form.Button, '_uprgade', _('Uprgade ') + _('Tiny File Manager'));
|
||||
o.inputtitle = _('Uprgade');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = function(ev, section_id) {
|
||||
const releasestag=this.section.getOption('_releaseslist').formvalue(section_id);
|
||||
//alert(releasestag);
|
||||
return fs.exec_direct('/usr/libexec/tinyfilemanager-update', [releasestag])
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message), 'error') });
|
||||
}
|
||||
};
|
||||
|
||||
// s = m.section(form.TypedSection, '_updater');
|
||||
// s.render = L.bind(function(view, section_id) {
|
||||
// return E('div',{ 'class': 'cbi-section' }, [
|
||||
// E('button', {
|
||||
// 'class': 'cbi-button cbi-button-action',
|
||||
// 'click': ui.createHandlerFn(view, 'handleQueryVendor')
|
||||
// }, _('Check update')),
|
||||
//
|
||||
// E('select', { 'class': 'cbi-input-select' }, [
|
||||
// E('option', { 'value': '2.4.7' }, '2.4.7'),
|
||||
// E('option', { 'value': '2.4.3' }, '2.4.3'),
|
||||
// E('option', { 'value': '2.4.1' }, '2.4.1')
|
||||
// ])
|
||||
// ]);
|
||||
// }, o, this);
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
load() {
|
||||
},
|
||||
|
||||
render() {
|
||||
const sid = L.env.sessionid || '';
|
||||
const url = window.location.protocol + '//' + window.location.hostname + '/tinyfilemanager/' + `?luci_sid=${sid}`;
|
||||
return E('iframe', {
|
||||
src: url,
|
||||
style: 'width: 100%; min-height: 100vh; border: none; border-radius: 3px;'
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/bin/bash
|
||||
# dependent: curl tar
|
||||
#
|
||||
# LuCI Tiny File Manager
|
||||
# Author: muink
|
||||
# Github: https://github.com/muink/luci-app-tinyfilemanager
|
||||
#
|
||||
|
||||
# PKGInfo
|
||||
REPOURL='https://github.com/prasathmani/tinyfilemanager'
|
||||
PKGNAME='tinyfilemanager'
|
||||
VERSION='2.6'
|
||||
#
|
||||
PKG_DIR=$PKGNAME-$VERSION
|
||||
REF_DIR="assets"
|
||||
#
|
||||
INDEXPHP="tinyfilemanager.php"
|
||||
#CFGSAMPl="config-sample.php"
|
||||
LANGFILE="translation.json"
|
||||
|
||||
|
||||
PROJDIR="$(cd "$(dirname "$0")"; pwd)" # <--
|
||||
WORKDIR="$PROJDIR/htdocs/$PKGNAME" # <--
|
||||
mkdir -p "$WORKDIR" 2>/dev/null
|
||||
cd $WORKDIR
|
||||
|
||||
|
||||
|
||||
# Clear Old version
|
||||
rm -rf *
|
||||
|
||||
# Download Repository
|
||||
grep -qE "^(master|main)$" <<< "$VERSION" && refs=heads || refs=tags
|
||||
curl -L https://codeload.github.com/prasathmani/tinyfilemanager/tar.gz/refs/$refs/$VERSION | tar -xvz -C "$WORKDIR"
|
||||
|
||||
# Version Check
|
||||
if [ -z "$(sed -En "/^\\\$external = array\(/,/^\);/p" "$PKG_DIR/$INDEXPHP")" ]; then
|
||||
online=y ver_cc=old # <= 2.5.3
|
||||
elif [ -n "$(sed -En "/^\\\$external = array\(/,/^\);/{s,^(.+=\")(http(s)?://.+/)([^/]+\.(css|js))(\".+),\4,p}" "$PKG_DIR/$INDEXPHP")" ]; then
|
||||
online=y ver_cc=master
|
||||
else
|
||||
online=n ver_cc=offline
|
||||
fi
|
||||
[ "$online" = "y" ] && {
|
||||
|
||||
# Preprocessing
|
||||
sed -Ei "/<link rel=\"(preconnect|dns-prefetch)\"/d" "$PKG_DIR/$INDEXPHP"
|
||||
[ "$ver_cc" = "old" ] && {
|
||||
FM_HIGHLIGHTJS_STYLE=$(sed -En "s|^\\\$highlightjs_style = *'([^']*)';|\1|p" "$PKG_DIR/$INDEXPHP")
|
||||
sed -i "s|<?php echo FM_HIGHLIGHTJS_STYLE ?>|\$FM_HIGHLIGHTJS_STYLE|g" "$PKG_DIR/$INDEXPHP"
|
||||
}
|
||||
[ "$ver_cc" = "master" ] && {
|
||||
__highlightjs_style=$(sed -En "s|^\\\$highlightjs_style = *'([^']*)';|\1|p" "$PKG_DIR/$INDEXPHP")
|
||||
sed -i "s|' . \$highlightjs_style . '|\$__highlightjs_style|" "$PKG_DIR/$INDEXPHP"
|
||||
}
|
||||
|
||||
# Download CDN Used
|
||||
mkdir -p "$REF_DIR" 2>/dev/null
|
||||
[ "$ver_cc" = "old" ] && refurl=($(sed -En "s,^.+=\"(https?://.+\.(css|js))\".+,\1,p" "$PKG_DIR/$INDEXPHP" | sort -u ))
|
||||
[ "$ver_cc" = "master" ] && refurl=($(sed -En "/^\\\$external /,/^\);/{s,^.+=\"(http(s)?://.+\.(css|js))\".+,\1, p}" "$PKG_DIR/$INDEXPHP" | sort -u ))
|
||||
ref=
|
||||
url=
|
||||
out=
|
||||
type=
|
||||
|
||||
# \.(css|js) main ref
|
||||
for _i in $(seq 0 1 $[ ${#refurl[@]} -1 ]); do
|
||||
eval "url=${refurl[$_i]}"
|
||||
out=${url##*/}
|
||||
type=${url##*.}
|
||||
|
||||
curl -Lo $out $url
|
||||
mkdir -p "$REF_DIR/$type" 2>/dev/null
|
||||
mv --backup $out "$REF_DIR/$type/"
|
||||
done
|
||||
|
||||
ref=$(for _p in $(find * -type f ! -path "$PKG_DIR/*"); do \
|
||||
sed -E "s/(,|;)/\1\n/g" $_p | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" >/dev/null; \
|
||||
[ "$?" == "0" ] && echo $_p; \
|
||||
done)
|
||||
|
||||
# ref of main ref
|
||||
for _i in $ref; do
|
||||
suburl=($(sed -E "s/(,|;)/\1\n/g" $_i | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" | sed -En "s|^[^']+'([^']+)'.+|\1| p"))
|
||||
hosturl=$(for _ in "${refurl[@]}"; do grep "${_i##*/}" <<< "$_"; done)
|
||||
|
||||
for _j in $(seq 0 1 $[ ${#suburl[@]} -1 ]); do
|
||||
url="${suburl[$_j]}"
|
||||
out=${url%%\?*}
|
||||
type=${hosturl##*.}
|
||||
|
||||
mkdir -p "$REF_DIR/$type/${out%/*}" 2>/dev/null
|
||||
curl -Lo ${out##*/} "${hosturl%/*}/$url"
|
||||
mv -f ${out##*/} "$REF_DIR/$type/$out"
|
||||
done
|
||||
done
|
||||
|
||||
# Post-processing
|
||||
[ "$ver_cc" = "old" ] && sed -i "s|\$FM_HIGHLIGHTJS_STYLE|<?php echo FM_HIGHLIGHTJS_STYLE ?>|g" "$PKG_DIR/$INDEXPHP"
|
||||
[ "$ver_cc" = "master" ] && sed -i "s|\$__highlightjs_style|' . \$highlightjs_style . '|" "$PKG_DIR/$INDEXPHP"
|
||||
|
||||
# Hotfix
|
||||
|
||||
# Migrating to Local Reference
|
||||
sed -Ei "s,^(.+=\")https?://.+/([^/]+\.(css|js))(\".+),\1$REF_DIR/\3/\2\4," "$PKG_DIR/$INDEXPHP"
|
||||
|
||||
}
|
||||
|
||||
# FixED
|
||||
[ "$ver_cc" = "master" ] || sed -Ei "/^if \(\\\$use_auth\) \{/,/^}/{/\/\/ Logging In/,/\/\/ Form/{s|(fm_redirect\().+|\1FM_SELF_URL);|g}}" "$PKG_DIR/$INDEXPHP"
|
||||
|
||||
# Clean up and Done
|
||||
[ -d "$PKG_DIR/$REF_DIR" ] && cp -rf "$PKG_DIR/$REF_DIR" .
|
||||
mv -f "$PKG_DIR/$INDEXPHP" ./index.php
|
||||
#mv -f "$PKG_DIR/$CFGSAMPl" .
|
||||
mv -f "$PKG_DIR/$LANGFILE" .
|
||||
rm -rf "$PKG_DIR"
|
||||
|
||||
# Package
|
||||
sed -Ei "/^VERSION=/{s|(VERSION:=)[^\}]*|\1$VERSION|}" "$PROJDIR/root/usr/libexec/tinyfilemanager-update"
|
||||
sed -Ei "s|(VERSION=).*|\1'$VERSION'|" "$PROJDIR/root/etc/init.d/tinyfilemanager"
|
||||
sed -Ei "s|(pkgversion =).*|\1 '$VERSION';|" "$PROJDIR/htdocs/luci-static/resources/view/tinyfilemanager/config.js"
|
||||
sed -Ei "s|(PKG_VERSION:=)[^-]+|\1$VERSION|" "$PROJDIR/Makefile"
|
||||
tar -czvf index.tgz * --owner=0 --group=0 --no-same-owner --no-same-permissions --remove-files
|
||||
@@ -1,206 +0,0 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:63
|
||||
msgid "Auto login user"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:64
|
||||
msgid ""
|
||||
"Automatically login/logout of the specified user based on LuCI credentials."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:161
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:162
|
||||
msgid "Check update"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:30
|
||||
msgid "Config"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:93
|
||||
msgid "D-M-YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:92
|
||||
msgid "D.M.YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:94
|
||||
msgid "D/M/YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:90
|
||||
msgid "DD-MM-YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:89
|
||||
msgid "DD.MM.YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:91
|
||||
msgid "DD/MM/YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:88
|
||||
msgid "Date format"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:129
|
||||
msgid "Disable"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:42
|
||||
msgid "Enable Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:141
|
||||
msgid "Enable proxy for updater"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:121
|
||||
msgid "Exclude Files/Folders"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:118
|
||||
msgid "Favicon path"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:78
|
||||
msgid "Global Readonly"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:130
|
||||
msgid "Google Docs"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json:3
|
||||
msgid "Grant access to tinyfilemanager procedures"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:106
|
||||
msgid "H:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:105
|
||||
msgid "HH:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:82
|
||||
msgid "Home path"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:45
|
||||
msgid "Login user name and passwd hash"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:135
|
||||
msgid "Max upload size (MBytes)"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:131
|
||||
msgid "Microsoft Web Apps"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:3
|
||||
msgid "NAS"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:127
|
||||
msgid "Online Docs viewer"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:144
|
||||
msgid "Proxy Protocol"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:154
|
||||
msgid "Proxy Server"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:55
|
||||
msgid "Readonly users"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:171
|
||||
msgid "Releases list"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:34
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:35
|
||||
msgid "Reload"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:128
|
||||
msgid "Requires running on open network"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:114
|
||||
msgid "Show seconds in time"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:108
|
||||
msgid "TT h:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:107
|
||||
msgid "TT hh:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:104
|
||||
msgid "Time format"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:178
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:11
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:22
|
||||
msgid "Tiny File Manager"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:178
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:179
|
||||
msgid "Uprgade"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:99
|
||||
msgid "YYYY-M-D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:96
|
||||
msgid "YYYY-MM-DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:98
|
||||
msgid "YYYY.M.D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:95
|
||||
msgid "YYYY.MM.DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:100
|
||||
msgid "YYYY/M/D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:97
|
||||
msgid "YYYY/MM/DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:46
|
||||
msgid ""
|
||||
"You can generate new passwd in <b>File Manager -> Admin -> Help -> Generate "
|
||||
"new</b> or <a href=\"%s\" target=\"_blank\"><b>Here</b></a>."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:110
|
||||
msgid "h:mm:ss TT"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:109
|
||||
msgid "hh:mm:ss TT"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:70
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
@@ -1 +0,0 @@
|
||||
zh_Hans
|
||||
@@ -1,222 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"PO-Revision-Date: 2023-03-26 21:32+0100\n"
|
||||
"Last-Translator: muink <hukk1996@gmail.com>\n"
|
||||
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
|
||||
"openwrt/luciapplicationstinyfilemanager/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"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:63
|
||||
msgid "Auto login user"
|
||||
msgstr "自动登入用户"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:64
|
||||
msgid ""
|
||||
"Automatically login/logout of the specified user based on LuCI credentials."
|
||||
msgstr "根据 LuCI 凭据自动登入/登出指定用户。"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:161
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:162
|
||||
msgid "Check update"
|
||||
msgstr "检查更新"
|
||||
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:30
|
||||
msgid "Config"
|
||||
msgstr "配置"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:93
|
||||
msgid "D-M-YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:92
|
||||
msgid "D.M.YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:94
|
||||
msgid "D/M/YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:90
|
||||
msgid "DD-MM-YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:89
|
||||
msgid "DD.MM.YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:91
|
||||
msgid "DD/MM/YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:88
|
||||
msgid "Date format"
|
||||
msgstr "日期格式"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:129
|
||||
msgid "Disable"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:42
|
||||
msgid "Enable Authentication"
|
||||
msgstr "启用用户验证"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:141
|
||||
msgid "Enable proxy for updater"
|
||||
msgstr "为更新器启用代理"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:121
|
||||
msgid "Exclude Files/Folders"
|
||||
msgstr "排除 文件/目录"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:118
|
||||
msgid "Favicon path"
|
||||
msgstr "Favicon 路径"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:78
|
||||
msgid "Global Readonly"
|
||||
msgstr "全局只读"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:130
|
||||
msgid "Google Docs"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json:3
|
||||
msgid "Grant access to tinyfilemanager procedures"
|
||||
msgstr "授予访问 LuCI 应用 tinyfilemanager 的权限"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:106
|
||||
msgid "H:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:105
|
||||
msgid "HH:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:82
|
||||
msgid "Home path"
|
||||
msgstr "家目录"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:45
|
||||
msgid "Login user name and passwd hash"
|
||||
msgstr "登入用户名和密码哈希"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:135
|
||||
msgid "Max upload size (MBytes)"
|
||||
msgstr "上传大小限制 (MBytes)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:131
|
||||
msgid "Microsoft Web Apps"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:3
|
||||
msgid "NAS"
|
||||
msgstr "存储"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:127
|
||||
msgid "Online Docs viewer"
|
||||
msgstr "在线文档预览器"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:144
|
||||
msgid "Proxy Protocol"
|
||||
msgstr "代理协议"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:154
|
||||
msgid "Proxy Server"
|
||||
msgstr "代理服务器"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:55
|
||||
msgid "Readonly users"
|
||||
msgstr "只读用户组"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:171
|
||||
msgid "Releases list"
|
||||
msgstr "发布版列表"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:34
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:35
|
||||
msgid "Reload"
|
||||
msgstr "重载"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:128
|
||||
msgid "Requires running on open network"
|
||||
msgstr "需要运行在开放网络"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:114
|
||||
msgid "Show seconds in time"
|
||||
msgstr "显示秒"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:108
|
||||
msgid "TT h:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:107
|
||||
msgid "TT hh:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:104
|
||||
msgid "Time format"
|
||||
msgstr "时间格式"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:178
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:11
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:22
|
||||
msgid "Tiny File Manager"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:178
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:179
|
||||
msgid "Uprgade"
|
||||
msgstr "升级"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:99
|
||||
msgid "YYYY-M-D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:96
|
||||
msgid "YYYY-MM-DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:98
|
||||
msgid "YYYY.M.D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:95
|
||||
msgid "YYYY.MM.DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:100
|
||||
msgid "YYYY/M/D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:97
|
||||
msgid "YYYY/MM/DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:46
|
||||
msgid ""
|
||||
"You can generate new passwd in <b>File Manager -> Admin -> Help -> Generate "
|
||||
"new</b> or <a href=\"%s\" target=\"_blank\"><b>Here</b></a>."
|
||||
msgstr ""
|
||||
"你可以在 <b>文件管理器 -> Admin -> 帮助 -> 生成新的</b> or <a href=\"%s\" "
|
||||
"target=\"_blank\"><b>这里</b></a>。"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:110
|
||||
msgid "h:mm:ss TT"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:109
|
||||
msgid "hh:mm:ss TT"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:70
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "To enable SSL support, you may need to install <b>%s</b><br/>"
|
||||
#~ msgstr "要启用 SSL 支持可能需先安装 <b>%s</b><br/>"
|
||||
@@ -1,222 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"PO-Revision-Date: 2023-03-26 21:32+0100\n"
|
||||
"Last-Translator: muink <hukk1996@gmail.com>\n"
|
||||
"Language-Team: Chinese (Traditional) <https://hosted.weblate.org/projects/"
|
||||
"openwrt/luciapplicationstinyfilemanager/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"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:63
|
||||
msgid "Auto login user"
|
||||
msgstr "自動登入用戶"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:64
|
||||
msgid ""
|
||||
"Automatically login/logout of the specified user based on LuCI credentials."
|
||||
msgstr "根據 LuCI 憑證自動登入/登出指定用戶。"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:161
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:162
|
||||
msgid "Check update"
|
||||
msgstr "檢查更新"
|
||||
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:30
|
||||
msgid "Config"
|
||||
msgstr "組態"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:93
|
||||
msgid "D-M-YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:92
|
||||
msgid "D.M.YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:94
|
||||
msgid "D/M/YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:90
|
||||
msgid "DD-MM-YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:89
|
||||
msgid "DD.MM.YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:91
|
||||
msgid "DD/MM/YYYY"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:88
|
||||
msgid "Date format"
|
||||
msgstr "日期格式"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:129
|
||||
msgid "Disable"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:42
|
||||
msgid "Enable Authentication"
|
||||
msgstr "啟用用戶認證"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:141
|
||||
msgid "Enable proxy for updater"
|
||||
msgstr "為更新器啟用代理"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:121
|
||||
msgid "Exclude Files/Folders"
|
||||
msgstr "排除 文件/目錄"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:118
|
||||
msgid "Favicon path"
|
||||
msgstr "Favicon 路徑"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:78
|
||||
msgid "Global Readonly"
|
||||
msgstr "全局只讀"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:130
|
||||
msgid "Google Docs"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/rpcd/acl.d/luci-app-tinyfilemanager.json:3
|
||||
msgid "Grant access to tinyfilemanager procedures"
|
||||
msgstr "授予訪問 LuCI 應用 tinyfilemanager 的權限"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:106
|
||||
msgid "H:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:105
|
||||
msgid "HH:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:82
|
||||
msgid "Home path"
|
||||
msgstr "家目錄"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:45
|
||||
msgid "Login user name and passwd hash"
|
||||
msgstr "登入用戶名和密碼雜湊"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:135
|
||||
msgid "Max upload size (MBytes)"
|
||||
msgstr "上傳大小限制 (MBytes)"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:131
|
||||
msgid "Microsoft Web Apps"
|
||||
msgstr ""
|
||||
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:3
|
||||
msgid "NAS"
|
||||
msgstr "存儲"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:127
|
||||
msgid "Online Docs viewer"
|
||||
msgstr "在線文檔預覽器"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:144
|
||||
msgid "Proxy Protocol"
|
||||
msgstr "代理協議"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:154
|
||||
msgid "Proxy Server"
|
||||
msgstr "代理伺服器"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:55
|
||||
msgid "Readonly users"
|
||||
msgstr "只讀用戶組"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:171
|
||||
msgid "Releases list"
|
||||
msgstr "發佈版本列表"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:34
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:35
|
||||
msgid "Reload"
|
||||
msgstr "重載"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:128
|
||||
msgid "Requires running on open network"
|
||||
msgstr "需要運行在開放網路"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:114
|
||||
msgid "Show seconds in time"
|
||||
msgstr "顯示秒"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:108
|
||||
msgid "TT h:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:107
|
||||
msgid "TT hh:mm:ss"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:104
|
||||
msgid "Time format"
|
||||
msgstr "時間格式"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:178
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:11
|
||||
#: root/usr/share/luci/menu.d/luci-app-tinyfilemanager.json:22
|
||||
msgid "Tiny File Manager"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:178
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:179
|
||||
msgid "Uprgade"
|
||||
msgstr "升級"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:99
|
||||
msgid "YYYY-M-D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:96
|
||||
msgid "YYYY-MM-DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:98
|
||||
msgid "YYYY.M.D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:95
|
||||
msgid "YYYY.MM.DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:100
|
||||
msgid "YYYY/M/D"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:97
|
||||
msgid "YYYY/MM/DD"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:46
|
||||
msgid ""
|
||||
"You can generate new passwd in <b>File Manager -> Admin -> Help -> Generate "
|
||||
"new</b> or <a href=\"%s\" target=\"_blank\"><b>Here</b></a>."
|
||||
msgstr ""
|
||||
"你可以在 <b>檔案管理器 -> Admin -> 幫助 -> 建立新的</b> or <a href=\"%s\" "
|
||||
"target=\"_blank\"><b>這裡</b></a>。"
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:110
|
||||
msgid "h:mm:ss TT"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:109
|
||||
msgid "hh:mm:ss TT"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/tinyfilemanager/config.js:70
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "To enable SSL support, you may need to install <b>%s</b><br/>"
|
||||
#~ msgstr "要啟用 SSL 支援可能需先安裝 <b>%s</b><br/>"
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
config main
|
||||
option use_auth '1'
|
||||
list auth_users 'admin:$2y$10$BewzfQXrlnUihprEgGt7ROMB9NigZcZkkwssIRYznF9fwMuObIZoa'
|
||||
list auth_users 'user:$2y$10$Fg6Dz8oH9fPoZ2jJan5tZuv6Z4Kp7avtQ9bDfrdRntXtPeiMAZyGO'
|
||||
list readonly_users 'user'
|
||||
option date_format 'd.m.o'
|
||||
option time_format 'H:i:s'
|
||||
option show_second '0'
|
||||
option favicon_path '/tinyfilemanager/rootfs/etc/tinyfilemanager/favicon.png'
|
||||
option online_viewer '0'
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2022-2023 muink
|
||||
|
||||
. "${IPKG_INSTROOT}/lib/functions.sh"
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
VERSION='2.6'
|
||||
|
||||
EXTRA_COMMANDS="check"
|
||||
EXTRA_HELP=\
|
||||
" check Check for version updates"
|
||||
|
||||
CONFIG_NAME='tinyfilemanager'
|
||||
TYPEDSECTION='main'
|
||||
|
||||
WORKDIR="/www/tinyfilemanager"
|
||||
VARDIR="/var/tinyfilemanager"
|
||||
CONF="/var/etc/tinyfilemanager.conf"
|
||||
|
||||
HOMEPATH="tinyfilemanager/rootfs"
|
||||
|
||||
if [ "$(uci -q get $CONFIG_NAME.@$TYPEDSECTION[0].proxy_enabled)" == "1" ]; then
|
||||
export ALL_PROXY=$(uci -q get $CONFIG_NAME.@$TYPEDSECTION[0].proxy_protocol)://$(uci -q get $CONFIG_NAME.@$TYPEDSECTION[0].proxy_server)
|
||||
fi
|
||||
|
||||
|
||||
|
||||
validate_section() {
|
||||
uci_load_validate $CONFIG_NAME $TYPEDSECTION "$1" "$2" \
|
||||
'defconf:string' \
|
||||
'use_auth:bool:0' \
|
||||
'auth_users:list(string)' \
|
||||
'readonly_users:list(string)' \
|
||||
'autologin_user:string' \
|
||||
'global_readonly:bool:0' \
|
||||
'root_path:directory' \
|
||||
'date_format:or("d.m.o", "d-m-o", "d/m/o", "j.n.o", "j-n-o", "j/n/o", "o.m.d", "o-m-d", "o/m/d", "o.n.j", "o-n-j", "o/n/j"):d.m.o' \
|
||||
'time_format:or("H\:i\:s", "G\:i\:s", "A h\:i\:s", "A g\:i\:s", "h\:i\:s A", "g\:i\:s A"):H\:i\:s' \
|
||||
'show_second:bool:0' \
|
||||
'favicon_path:string' \
|
||||
'exclude_items:list(string)' \
|
||||
'online_viewer:or("0", "google", "microsoft"):0' \
|
||||
'max_upload_size:and(uinteger,max(2048))'
|
||||
}
|
||||
|
||||
init_config() {
|
||||
local defconf="$(sed -n '/^\/\/Default Configuration/,/^$/p' "$WORKDIR/index.php" | tr '\n' '|' | sed 's,|,\\n,g')"
|
||||
|
||||
sed -n "/\/\/ --- EDIT BELOW CONFIGURATION CAREFULLY ---/,/\/\/ --- EDIT BELOW CAREFULLY OR DO NOT EDIT AT ALL ---/{ \
|
||||
s|// --- EDIT BELOW CONFIGURATION CAREFULLY ---|\<?php\n$defconf|; \
|
||||
s|// --- EDIT BELOW CAREFULLY OR DO NOT EDIT AT ALL ---|?\>|; \
|
||||
/\/\/ if User has the external config file, try to use it to override the default config above/,/^}/d; \
|
||||
/^define(/d; \
|
||||
p \
|
||||
}" "$WORKDIR/index.php" > "$1"
|
||||
|
||||
sed -Ei "s|(^\\\$root_path =)(.+)|\1 \\\$_SERVER['DOCUMENT_ROOT'].'/$HOMEPATH';|; \
|
||||
s|(^\\\$root_url =)(.+)|\1 '$HOMEPATH';|; \
|
||||
|
||||
s|(^\\\$default_timezone =)(.+)|\1 '$(uci -q get system.@system[0].zonename)';| \
|
||||
" "$1"
|
||||
}
|
||||
|
||||
apply_config() {
|
||||
[ "$2" == "0" ] || { >&2 echo "section $1 validation failed"; return 1; }
|
||||
|
||||
local _conf="$CONF"
|
||||
|
||||
local auth_users=$(echo "$auth_users"|sed -E "s/('[^']+'|[^' ]+)/'\1'/g; s|:|' => '|g; s|''|'|g; s|' '|',\\\n '|g; s|^| |")
|
||||
local readonly_users=$(echo "$readonly_users"|sed -E "s/('[^']+'|[^' ]+)/'\1'/g; s|''|'|g; s|' '|',\\\n '|g; s|^| |")
|
||||
|
||||
local exclude_items=$(echo "$exclude_items"|sed -E "s/('[^']+'|[^' ]+)/'\1'/g; s|''|'|g; s|' '|',\\\n '|g; s|^| |")
|
||||
|
||||
[ -n "$defconf" ] && sed -Ei "s|^\\\$CONFIG\s*=.*|\$CONFIG = '$defconf'\;|" "$_conf"
|
||||
[ "$use_auth" -eq "1" ] && sed -Ei "s|(^\\\$use_auth =)(.+)|\1 true;|" "$_conf" || sed -Ei "s|(^\\\$use_auth =)(.+)|\1 false;|" "$_conf"
|
||||
[ -n "$autologin_user" ] && sed -Ei "s|\?>|\$luci_autologin_user = '$autologin_user';\ninclude '/usr/share/tinyfilemanager/auto-login-base-luci.php';\n\n?>|" "$_conf"
|
||||
[ "$global_readonly" -eq "1" ] && sed -Ei "s|(^\\\$global_readonly =)(.+)|\1 true;|" "$_conf" || sed -Ei "s|(^\\\$global_readonly =)(.+)|\1 false;|" "$_conf"
|
||||
sed -Ei "/^\\\$auth_users /,/\);/{/^ /d}" "$_conf" && sed -Ei "/^\\\$auth_users /a\ $auth_users" "$_conf"
|
||||
sed -Ei "/^\\\$readonly_users /,/\);/{/^ /d}" "$_conf" && sed -Ei "/^\\\$readonly_users /a\ $readonly_users" "$_conf"
|
||||
sed -Ei "/^\\\$exclude_items /{s|array\(\);|array(\n);|}" "$_conf" && sed -Ei "/^\\\$exclude_items /a\ $exclude_items" "$_conf"
|
||||
sed -Ei "s|(^\\\$default_timezone =)(.+)|\1 '$(uci -q get system.@system[0].zonename)';|" "$_conf"
|
||||
if [ -n "$root_path" ]; then
|
||||
sed -Ei "s|(^\\\$root_path =)(.+)|\1 \\\$_SERVER['DOCUMENT_ROOT'].'/${HOMEPATH}$root_path';|; \
|
||||
s|(^\\\$root_url =)(.+)|\1 '${HOMEPATH}$root_path';| \
|
||||
" "$_conf"
|
||||
fi
|
||||
sed -Ei "s|(^\\\$datetime_format =)(.+)|\1 '$date_format $time_format';|" "$_conf"
|
||||
[ "$show_second" -eq "0" ] && sed -Ei "/^\\\$datetime_format =/{s|:s||}" "$_conf"
|
||||
sed -Ei "s|(^\\\$favicon_path =)(.+)|\1 '$favicon_path';|" "$_conf"
|
||||
if [ "$online_viewer" == "0" ]; then sed -Ei "s|(^\\\$online_viewer =)(.+)|\1 false;|" "$_conf"
|
||||
else sed -Ei "s|(^\\\$online_viewer =)(.+)|\1 '$online_viewer';|" "$_conf"
|
||||
fi
|
||||
if [ "$(( $max_upload_size +0 ))" == "0" ]; then sed -Ei "s|(^\\\$max_upload_size_bytes =)(.+)|\1 26214400;|" "$_conf" #25M
|
||||
else sed -Ei "s|(^\\\$max_upload_size_bytes =)(.+)|\1 $(( $max_upload_size * 1024**2 ));|" "$_conf"
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
mkdir /var/etc 2>/dev/null
|
||||
touch "$CONF"
|
||||
ln -s "$CONF" "$WORKDIR/config.php" 2>/dev/null
|
||||
init_config "$CONF"
|
||||
|
||||
config_load "$CONFIG_NAME"
|
||||
config_foreach validate_section "$TYPEDSECTION" apply_config
|
||||
|
||||
# CONFIG save
|
||||
procd_open_instance "config-save"
|
||||
procd_set_param command "/usr/libexec/tinyfilemanager-config-save"
|
||||
procd_set_param env CONF=$CONF
|
||||
procd_set_param respawn
|
||||
#procd_set_param stdout 1
|
||||
#procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIG_NAME" 'system'
|
||||
}
|
||||
|
||||
check() {
|
||||
local owner='prasathmani'
|
||||
local repo='tinyfilemanager'
|
||||
mkdir $VARDIR 2>/dev/null
|
||||
curl -sSL https://api.github.com/repos/$owner/$repo/releases \
|
||||
| grep '"tag_name":' | sed "/$VERSION/,\${/$VERSION/b;d}" \
|
||||
| sed -E 's|",||g; s|^.+"([^"]+)|\1|g' \
|
||||
> "$VARDIR/releaseslist"
|
||||
[ "$?" != 0 ] || sed -i 'i\master' "$VARDIR/releaseslist"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
restart "$@"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
location /tinyfilemanager/ {
|
||||
index index.php;
|
||||
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
|
||||
# Mitigate https://httpoxy.org/ vulnerabilities
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
|
||||
#error_log /dev/null;
|
||||
fastcgi_connect_timeout 300s;
|
||||
fastcgi_read_timeout 300s;
|
||||
fastcgi_send_timeout 300s;
|
||||
fastcgi_buffer_size 32k;
|
||||
fastcgi_buffers 4 32k;
|
||||
fastcgi_busy_buffers_size 32k;
|
||||
fastcgi_temp_file_write_size 32k;
|
||||
client_body_timeout 10s;
|
||||
send_timeout 60s; # default, increase if experiencing a lot of timeouts.
|
||||
output_buffers 1 32k;
|
||||
include fastcgi_params;
|
||||
|
||||
# Only throw it at PHP-FPM if file exists (prevents PHP exploits).
|
||||
fastcgi_pass 127.0.0.1:1026; # or: unix:/var/run/php-fpm.sock;
|
||||
# fastcgi_pass unix:/var/run/php8-fpm.sock;
|
||||
|
||||
# SCRIPT_FILENAME parameter is used for PHP FPM determining
|
||||
# the script name. If it is not set in fastcgi_params file,
|
||||
# i.e. /etc/nginx/fastcgi_params or in the parent contexts,
|
||||
# please comment off following line:
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 234 B |
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# unpack
|
||||
tar -C /www/tinyfilemanager -xzf /www/tinyfilemanager/index.tgz
|
||||
rm -f /www/tinyfilemanager/index.tgz
|
||||
|
||||
# uhttpd
|
||||
uci -q get uhttpd.main.index_page|grep -i 'index.php' >/dev/null || uci add_list uhttpd.main.index_page='index.php'
|
||||
uci -q get uhttpd.main.interpreter|grep -i '.php=/usr/bin/php-cgi' >/dev/null || uci add_list uhttpd.main.interpreter='.php=/usr/bin/php-cgi'
|
||||
uci changes uhttpd|grep . >/dev/null && uci commit uhttpd
|
||||
/etc/init.d/uhttpd reload
|
||||
|
||||
# nginx
|
||||
[ -n "$(command -v nginx)" ] && /etc/init.d/nginx reload
|
||||
|
||||
exit 0
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
CONFIG_NAME='tinyfilemanager'
|
||||
|
||||
while :; do
|
||||
inotifywait -e modify $CONF 2>/dev/null
|
||||
|
||||
CONFIG="$(sed -En "/^\\\$CONFIG\s*=.*/{s|^.+'(.+)'.*|\1|p}" $CONF)"
|
||||
if [ -n "$CONFIG" ]; then
|
||||
uci set $CONFIG_NAME.@$TYPEDSECTION[0].defconf="$CONFIG"
|
||||
[ -z "$(uci changes $CONFIG_NAME)" ] || uci commit $CONFIG_NAME
|
||||
fi
|
||||
done
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/bin/bash
|
||||
# dependent: curl tar
|
||||
#
|
||||
# LuCI Tiny File Manager
|
||||
# Author: muink
|
||||
# Github: https://github.com/muink/luci-app-tinyfilemanager
|
||||
#
|
||||
|
||||
# PKGInfo
|
||||
REPOURL='https://github.com/prasathmani/tinyfilemanager'
|
||||
PKGNAME='tinyfilemanager'
|
||||
VERSION="$1"; VERSION="${VERSION:=2.6}"
|
||||
#
|
||||
PKG_DIR=$PKGNAME-$VERSION
|
||||
REF_DIR="assets"
|
||||
#
|
||||
INDEXPHP="tinyfilemanager.php"
|
||||
#CFGSAMPl="config-sample.php"
|
||||
LANGFILE="translation.json"
|
||||
|
||||
|
||||
WORKDIR="/www/$PKGNAME"
|
||||
mkdir -p "$WORKDIR" 2>/dev/null
|
||||
cd $WORKDIR
|
||||
|
||||
if [ "$(uci -q get $PKGNAME.@main[0].proxy_enabled)" == "1" ]; then
|
||||
export ALL_PROXY=$(uci -q get $PKGNAME.@main[0].proxy_protocol)://$(uci -q get $PKGNAME.@main[0].proxy_server)
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Clear Old version
|
||||
rm -rf *
|
||||
|
||||
# Download Repository
|
||||
grep -qE "^(master|main)$" <<< "$VERSION" && refs=heads || refs=tags
|
||||
curl -sSL https://codeload.github.com/prasathmani/tinyfilemanager/tar.gz/refs/$refs/$VERSION | tar -xvz -C /tmp
|
||||
|
||||
# Version Check
|
||||
if [ -z "$(sed -En "/^\\\$external = array\(/,/^\);/p" /tmp/$PKG_DIR/$INDEXPHP)" ]; then
|
||||
online=y ver_cc=old # <= 2.5.3
|
||||
elif [ -n "$(sed -En "/^\\\$external = array\(/,/^\);/{s,^(.+=\")(http(s)?://.+/)([^/]+\.(css|js))(\".+),\4,p}" /tmp/$PKG_DIR/$INDEXPHP)" ]; then
|
||||
online=y ver_cc=master
|
||||
else
|
||||
online=n ver_cc=offline
|
||||
fi
|
||||
[ "$online" = "y" ] && {
|
||||
|
||||
# Preprocessing
|
||||
sed -Ei "/<link rel=\"(preconnect|dns-prefetch)\"/d" /tmp/$PKG_DIR/$INDEXPHP
|
||||
[ "$ver_cc" = "old" ] && {
|
||||
FM_HIGHLIGHTJS_STYLE=$(sed -En "s|^\\\$highlightjs_style = *'([^']*)';|\1|p" /tmp/$PKG_DIR/$INDEXPHP)
|
||||
sed -i "s|<?php echo FM_HIGHLIGHTJS_STYLE ?>|\$FM_HIGHLIGHTJS_STYLE|g" /tmp/$PKG_DIR/$INDEXPHP
|
||||
}
|
||||
[ "$ver_cc" = "master" ] && {
|
||||
__highlightjs_style=$(sed -En "s|^\\\$highlightjs_style = *'([^']*)';|\1|p" /tmp/$PKG_DIR/$INDEXPHP)
|
||||
sed -i "s|' . \$highlightjs_style . '|\$__highlightjs_style|" /tmp/$PKG_DIR/$INDEXPHP
|
||||
}
|
||||
|
||||
# Download CDN Used
|
||||
mkdir -p "$REF_DIR" 2>/dev/null
|
||||
[ "$ver_cc" = "old" ] && refurl=($(sed -En "s,^.+=\"(https?://.+\.(css|js))\".+,\1,p" /tmp/$PKG_DIR/$INDEXPHP | sort -u ))
|
||||
[ "$ver_cc" = "master" ] && refurl=($(sed -En "/^\\\$external /,/^\);/{s,^.+=\"(http(s)?://.+\.(css|js))\".+,\1, p}" /tmp/$PKG_DIR/$INDEXPHP | sort -u ))
|
||||
ref=
|
||||
url=
|
||||
out=
|
||||
path=
|
||||
|
||||
# \.(css|js) main ref
|
||||
for _i in $(seq 0 1 $[ ${#refurl[@]} -1 ]); do
|
||||
eval "url=${refurl[$_i]}"
|
||||
out=${url##*/}
|
||||
path="$REF_DIR/${url#http*://}"
|
||||
|
||||
mkdir -p "${path%/*}" 2>/dev/null
|
||||
curl -sSLo $out $url
|
||||
mv -f $out "${path%/*}"
|
||||
done
|
||||
|
||||
ref=$(for _p in $(find * -type f); do \
|
||||
sed -E "s/(,|;)/\1\n/g" $_p | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" >/dev/null; \
|
||||
[ "$?" == "0" ] && echo $_p; \
|
||||
done)
|
||||
|
||||
# ref of main ref
|
||||
for _i in $ref; do
|
||||
suburl=($(sed -E "s/(,|;)/\1\n/g" $_i | grep -E "\burl\([^\)]+\)" | grep -Ev "\burl\(\"data:image" | sed -En "s|^[^']+'([^']+)'.+|\1| p"))
|
||||
hosturl=$(for _ in "${refurl[@]}"; do grep "${_i##*/}" <<< "$_"; done)
|
||||
|
||||
for _j in $(seq 0 1 $[ ${#suburl[@]} -1 ]); do
|
||||
url="${suburl[$_j]}"
|
||||
out=${url%%\?*}
|
||||
path="$REF_DIR/${hosturl#http*://}"
|
||||
|
||||
mkdir -p "${path%/*}/${out%/*}" 2>/dev/null
|
||||
curl -sSLo ${out##*/} "${hosturl%/*}/$url"
|
||||
mv -f ${out##*/} "${path%/*}/$out"
|
||||
done
|
||||
done
|
||||
|
||||
# Post-processing
|
||||
[ "$ver_cc" = "old" ] && sed -i "s|\$FM_HIGHLIGHTJS_STYLE|<?php echo FM_HIGHLIGHTJS_STYLE ?>|g" /tmp/$PKG_DIR/$INDEXPHP
|
||||
[ "$ver_cc" = "master" ] && sed -i "s|\$__highlightjs_style|' . \$highlightjs_style . '|" /tmp/$PKG_DIR/$INDEXPHP
|
||||
|
||||
# Hotfix
|
||||
|
||||
# Migrating to Local Reference
|
||||
sed -Ei "s,^(.+=\")https?://(.+\.(css|js))(\".+),\1$REF_DIR/\2\4," /tmp/$PKG_DIR/$INDEXPHP
|
||||
|
||||
}
|
||||
|
||||
# FixED
|
||||
[ "$ver_cc" = "master" ] || sed -Ei "/^if \(\\\$use_auth\) \{/,/^}/{/\/\/ Logging In/,/\/\/ Form/{s|(fm_redirect\().+|\1FM_SELF_URL);|g}}" /tmp/$PKG_DIR/$INDEXPHP
|
||||
|
||||
# Clean up and Done
|
||||
[ -d "/tmp/$PKG_DIR/$REF_DIR" ] && cp -rf "/tmp/$PKG_DIR/$REF_DIR" .
|
||||
mv -f /tmp/$PKG_DIR/$INDEXPHP ./index.php
|
||||
#mv -f /tmp/$PKG_DIR/$CFGSAMPl .
|
||||
mv -f /tmp/$PKG_DIR/$LANGFILE .
|
||||
find * -type d -exec chmod 755 {} \;
|
||||
find * -type f -exec chmod 644 {} \;
|
||||
[ ! -d /www/tinyfilemanager/rootfs ] && ln -s / /www/tinyfilemanager/rootfs
|
||||
/etc/init.d/tinyfilemanager start
|
||||
|
||||
|
||||
|
||||
#$(INSTALL_DIR) $(1)/usr/libexec
|
||||
#$(INSTALL_DIR) $(1)/www/$PKGNAME
|
||||
#$(INSTALL_BIN) run.sh $(1)/usr/libexec/$PKGNAME
|
||||
#$(CP) $PKG_DIR/$INDEXPHP $(1)/www/$PKGNAME/index.php
|
||||
#$(CP) $PKG_DIR/$CFGSAMPl $(1)/www/$PKGNAME/$CFGSAMPl
|
||||
#$(CP) $PKG_DIR/$LANGFILE $(1)/www/$PKGNAME/$LANGFILE
|
||||
#$(CP) --parents -rf $REF_DIR/ $(1)/www/$PKGNAME/
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"admin/nas": {
|
||||
"title": "NAS",
|
||||
"order": 44,
|
||||
"action": {
|
||||
"type": "firstchild",
|
||||
"recurse": true
|
||||
}
|
||||
},
|
||||
"admin/nas/tinyfilemanager": {
|
||||
"title": "Tiny File Manager",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-tinyfilemanager" ],
|
||||
"uci": { "tinyfilemanager": true }
|
||||
}
|
||||
},
|
||||
"admin/nas/tinyfilemanager/tinyfilemanager": {
|
||||
"title": "Tiny File Manager",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "tinyfilemanager/main"
|
||||
}
|
||||
},
|
||||
"admin/nas/tinyfilemanager/config": {
|
||||
"title": "Config",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "tinyfilemanager/config"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"luci-app-tinyfilemanager": {
|
||||
"description": "Grant access to tinyfilemanager procedures",
|
||||
"read": {
|
||||
"file": {
|
||||
"/usr/libexec/tinyfilemanager-update": [ "exec" ],
|
||||
"/etc/init.d/tinyfilemanager check": [ "exec" ],
|
||||
"/etc/init.d/tinyfilemanager reload": [ "exec" ],
|
||||
"/var/tinyfilemanager/releaseslist": [ "read" ],
|
||||
"/tmp/tinyfilemanager/releaseslist": [ "read" ]
|
||||
},
|
||||
"uci": [ "tinyfilemanager" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "tinyfilemanager" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
// Validates LuCI session via ubus on every request.
|
||||
// Auto-login when accessed from LuCI menu (luci_sid passed in URL).
|
||||
// Forces logout when LuCI session expires.
|
||||
|
||||
if (!defined('FM_SESSION_ID')) define('FM_SESSION_ID', 'filemanager');
|
||||
session_name('filemanager');
|
||||
session_start();
|
||||
|
||||
function luci_validate_sid($sid) {
|
||||
if (empty($sid) || !preg_match('/^[a-f0-9]{32}$/', $sid)) return false;
|
||||
$out = shell_exec('ubus call session get \'{"ubus_rpc_session":"' . $sid . '"}\' 2>/dev/null');
|
||||
return $out && strpos($out, '"username"') !== false;
|
||||
}
|
||||
|
||||
// Step 1: luci_sid in URL → validate and auto-login
|
||||
if (!empty($_GET['luci_sid'])) {
|
||||
$sid = preg_replace('/[^a-f0-9]/', '', $_GET['luci_sid']);
|
||||
if (luci_validate_sid($sid)) {
|
||||
$_SESSION['filemanager']['logged'] = $luci_autologin_user;
|
||||
$_SESSION['filemanager']['luci_sid'] = $sid;
|
||||
session_write_close();
|
||||
header('Location: /tinyfilemanager/');
|
||||
exit;
|
||||
} else {
|
||||
unset($_SESSION['filemanager']['logged']);
|
||||
unset($_SESSION['filemanager']['luci_sid']);
|
||||
session_write_close();
|
||||
http_response_code(403);
|
||||
die('LuCI session invalid or expired.');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: On every request, re-validate stored LuCI session
|
||||
if (!empty($_SESSION['filemanager']['logged'])) {
|
||||
$sid = $_SESSION['filemanager']['luci_sid'] ?? '';
|
||||
if (!luci_validate_sid($sid)) {
|
||||
// LuCI session expired → force logout
|
||||
unset($_SESSION['filemanager']['logged']);
|
||||
unset($_SESSION['filemanager']['luci_sid']);
|
||||
session_write_close();
|
||||
header('Location: /tinyfilemanager/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
session_write_close();
|
||||
|
||||
?>
|
||||
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vantage
|
||||
PKG_VERSION:=1.0.14
|
||||
PKG_RELEASE:=22
|
||||
PKG_RELEASE:=23
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage dashboard
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"admin/dashboard": {
|
||||
"admin/status/dashboard": {
|
||||
"title": "Dashboard",
|
||||
"order": 1,
|
||||
"action": {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
|
||||
# Markdown may use trailing spaces for hard line breaks.
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,19 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.sh text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
Makefile text eol=lf
|
||||
|
||||
*.apk binary
|
||||
*.ipk binary
|
||||
*.bin binary
|
||||
*.img binary
|
||||
*.gz binary
|
||||
*.zst binary
|
||||
*.pem text eol=lf
|
||||
|
||||
# Shared scripts vendored from repo-templates: kept out of the language stats.
|
||||
scripts/check-readme.sh linguist-vendored
|
||||
scripts/release-notes.sh linguist-vendored
|
||||
@@ -1,3 +0,0 @@
|
||||
/build/
|
||||
/dist/
|
||||
.DS_Store
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Nikitid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,59 +0,0 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wrqr
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_MAINTAINER:=nikitid
|
||||
PKGARCH:=all
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-wrqr
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=Wi-Fi QR codes for Status Overview
|
||||
DEPENDS:=+luci-base +luci-lib-uqr
|
||||
endef
|
||||
|
||||
define Package/luci-app-wrqr/description
|
||||
Display QR codes for active Wi-Fi access points on the LuCI Status Overview.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-wrqr/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./luci/acl.json $(1)/usr/share/rpcd/acl.d/luci-app-wrqr.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/wrqr
|
||||
$(INSTALL_DATA) ./luci/shared.js $(1)/www/luci-static/resources/wrqr/shared.js
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/status/include
|
||||
$(INSTALL_DATA) ./luci/overview.js $(1)/www/luci-static/resources/view/status/include/15_wifi-qr.js
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/licenses/luci-app-wrqr
|
||||
$(INSTALL_DATA) ./LICENSE $(1)/usr/share/licenses/luci-app-wrqr/LICENSE
|
||||
endef
|
||||
|
||||
define Package/luci-app-wrqr/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT:-}" ] && exit 0
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-wrqr/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT:-}" ] && exit 0
|
||||
case "$${1:-}" in upgrade) exit 0 ;; esac
|
||||
[ "$${PKG_UPGRADE:-0}" = 1 ] && exit 0
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-wrqr))
|
||||
@@ -1,78 +0,0 @@
|
||||
# Wi-Fi QR for OpenWrt
|
||||
|
||||
[Русский](README.ru.md)
|
||||
|
||||
[](https://github.com/Nikitid/luci-app-wrqr/actions/workflows/ci.yml)
|
||||
[](https://github.com/Nikitid/luci-app-wrqr/releases/latest)
|
||||
[](LICENSE)
|
||||
|
||||
The `luci-app-wrqr` package adds a widget with Wi-Fi QR codes to the LuCI
|
||||
**Status -> Overview** page.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- one QR code for each distinct active access point;
|
||||
- identical networks broadcast by several radios share one code;
|
||||
- disabled, unsupported and non-AP interfaces are left out.
|
||||
|
||||
## Requirements
|
||||
|
||||
- OpenWrt 24.10 (IPK) or OpenWrt 25.12 (APK);
|
||||
- LuCI.
|
||||
|
||||
## Installation
|
||||
|
||||
### OpenWrt 24.10
|
||||
|
||||
Download the latest `luci-app-wrqr_*_all.ipk` from
|
||||
[Releases](https://github.com/Nikitid/luci-app-wrqr/releases) and upload it
|
||||
through **System -> Software -> Upload Package**.
|
||||
|
||||
### OpenWrt 25.12
|
||||
|
||||
```sh
|
||||
wget -O /tmp/nikitid-feed.sh \
|
||||
https://raw.githubusercontent.com/Nikitid/openwrt-feed/feed/install.sh
|
||||
sh /tmp/nikitid-feed.sh luci-app-wrqr
|
||||
```
|
||||
|
||||
The installer verifies the publisher key, adds the shared signed Nikitid
|
||||
application repository and installs only the named package. To update:
|
||||
|
||||
```sh
|
||||
apk update
|
||||
apk upgrade luci-app-wrqr
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
The widget reads fresh UCI and wireless runtime state on LuCI's normal status
|
||||
poll. It only reads: it restarts nothing and writes nothing - not Wi-Fi, rpcd,
|
||||
uhttpd or the router.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
./scripts/ci-check.sh
|
||||
```
|
||||
|
||||
Building, signing and releasing: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Repository map](docs/MAP.md) - where things live
|
||||
- [Development](docs/DEVELOPMENT.md) - building, signing and releasing
|
||||
|
||||
## Support
|
||||
|
||||
Questions and bug reports go to
|
||||
[Issues](https://github.com/Nikitid/luci-app-wrqr/issues/new/choose): pick the form that
|
||||
fits. Report a vulnerability privately through
|
||||
[a security advisory](https://github.com/Nikitid/luci-app-wrqr/security/advisories/new).
|
||||
English or Russian is fine.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -1,78 +0,0 @@
|
||||
# Wi-Fi QR для OpenWrt
|
||||
|
||||
[English](README.md)
|
||||
|
||||
[](https://github.com/Nikitid/luci-app-wrqr/actions/workflows/ci.yml)
|
||||
[](https://github.com/Nikitid/luci-app-wrqr/releases/latest)
|
||||
[](LICENSE)
|
||||
|
||||
Пакет `luci-app-wrqr` добавляет на страницу LuCI **Status -> Overview** виджет с
|
||||
QR-кодами для подключения к Wi-Fi.
|
||||
|
||||

|
||||
|
||||
## Возможности
|
||||
|
||||
- по одному QR-коду на каждую различающуюся активную точку доступа;
|
||||
- одинаковые сети, вещаемые несколькими радиомодулями, объединяются в один код;
|
||||
- отключённые, неподдерживаемые и не-AP интерфейсы не показываются.
|
||||
|
||||
## Требования
|
||||
|
||||
- OpenWrt 24.10 (IPK) или OpenWrt 25.12 (APK);
|
||||
- LuCI.
|
||||
|
||||
## Установка
|
||||
|
||||
### OpenWrt 24.10
|
||||
|
||||
Скачайте последний `luci-app-wrqr_*_all.ipk` из
|
||||
[Releases](https://github.com/Nikitid/luci-app-wrqr/releases) и загрузите его
|
||||
через **System -> Software -> Upload Package**.
|
||||
|
||||
### OpenWrt 25.12
|
||||
|
||||
```sh
|
||||
wget -O /tmp/nikitid-feed.sh \
|
||||
https://raw.githubusercontent.com/Nikitid/openwrt-feed/feed/install.sh
|
||||
sh /tmp/nikitid-feed.sh luci-app-wrqr
|
||||
```
|
||||
|
||||
Установщик проверяет ключ издателя, подключает общий подписанный репозиторий
|
||||
приложений Nikitid и ставит только указанный пакет. Обновление:
|
||||
|
||||
```sh
|
||||
apk update
|
||||
apk upgrade luci-app-wrqr
|
||||
```
|
||||
|
||||
## Как это работает
|
||||
|
||||
Виджет читает свежее состояние UCI и беспроводного стека на обычном опросе
|
||||
статуса LuCI. Он только читает: ничего не перезапускает и ничего не
|
||||
записывает - ни Wi-Fi, ни rpcd, ни uhttpd, ни роутер.
|
||||
|
||||
## Разработка
|
||||
|
||||
```sh
|
||||
./scripts/ci-check.sh
|
||||
```
|
||||
|
||||
Сборка, подпись и выпуск: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
||||
|
||||
## Документация
|
||||
|
||||
- [Карта репозитория](docs/MAP.md) - где что лежит
|
||||
- [Разработка](docs/DEVELOPMENT.md) - сборка, подпись и выпуск
|
||||
|
||||
## Поддержка
|
||||
|
||||
Вопросы и сообщения об ошибках - в
|
||||
[Issues](https://github.com/Nikitid/luci-app-wrqr/issues/new/choose), выберите
|
||||
подходящую форму. Об уязвимости сообщайте приватно через
|
||||
[security advisory](https://github.com/Nikitid/luci-app-wrqr/security/advisories/new).
|
||||
Можно писать по-русски или по-английски.
|
||||
|
||||
## Лицензия
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -1,9 +0,0 @@
|
||||
# OpenWrt 25.12 build and shared feed identity.
|
||||
OPENWRT_APK_VERSION=25.12.5
|
||||
OPENWRT_APK_TARGET=mediatek/filogic
|
||||
OPENWRT_APK_ARCH=aarch64_cortex-a53
|
||||
OPENWRT_APK_SDK_ARCHIVE=openwrt-sdk-25.12.5-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64.tar.zst
|
||||
OPENWRT_APK_SDK_SHA256=ff4a38a397caa2cfe1c39e18f84ddede14878221b3593c3f2c4cfe24e3ec4c25
|
||||
OPENWRT_APK_KEY_FILE=keys/nikitid-openwrt-release.pem
|
||||
OPENWRT_APK_TRUST_SHA256=f27474d9261f1084350cf4ba34ecdff29e533769c36483d8dd85566e30a6a703
|
||||
OPENWRT_FEED_REPOSITORY=Nikitid/openwrt-feed
|
||||
@@ -1,34 +0,0 @@
|
||||
# Development
|
||||
|
||||
## Checks
|
||||
|
||||
```sh
|
||||
./scripts/ci-check.sh
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
The IPK for OpenWrt 24.10 is written to `dist/` by `scripts/build-ipk.sh`. The
|
||||
APK for OpenWrt 25.12 needs a compatible SDK:
|
||||
|
||||
```sh
|
||||
OPENWRT_SDK_DIR=/path/to/openwrt-sdk-25.12.5-mediatek-filogic \
|
||||
./scripts/build-apk.sh
|
||||
```
|
||||
|
||||
A release APK is signed with the shared publisher key by
|
||||
`scripts/build-apk-release.sh`, which reads the key from
|
||||
`OPENWRT_APK_SIGNING_KEY`.
|
||||
|
||||
The private half of the publisher key is not stored in any repository. This
|
||||
repository builds and signs only its own package and publishes it as a release
|
||||
asset; the index is assembled by
|
||||
[Nikitid/openwrt-feed](https://github.com/Nikitid/openwrt-feed). See
|
||||
[Joining the shared feed](https://github.com/Nikitid/openwrt-feed/blob/main/docs/MEMBER_INTEGRATION.md).
|
||||
|
||||
## Release
|
||||
|
||||
Push a `vX.Y.Z` tag matching the version in `release.env`. The release workflow
|
||||
checks the tag, runs `scripts/ci-check.sh`, builds and signs the APK with the
|
||||
pinned OpenWrt SDK, and publishes the release with notes made by
|
||||
`scripts/release-notes.sh` from the commit subjects since the previous tag.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Repository map
|
||||
|
||||
Where things live. The whole application is two files under `luci/`.
|
||||
|
||||
## The shape of it
|
||||
|
||||
One OpenWrt package, `luci-app-wrqr`: a Wi-Fi QR widget on LuCI's Status →
|
||||
Overview page. It shows one QR code per distinct active access-point
|
||||
configuration and merges identical networks broadcast by several radios.
|
||||
|
||||
| file | owns |
|
||||
| --- | --- |
|
||||
| `luci/overview.js` | the widget: reads UCI and wireless runtime state, renders the codes |
|
||||
| `luci/shared.js` | the design system and the dictionary |
|
||||
| `luci/acl.json` | the calls the widget is allowed to make |
|
||||
| `Makefile`, `release.env`, `apk-feed.env` | packaging and release identity |
|
||||
| `scripts/` | build, packaging and the checks |
|
||||
|
||||
The widget is read-only by design: it restarts nothing and writes nothing.
|
||||
Unsupported, disabled and non-AP interfaces are omitted rather than shown in
|
||||
an error state.
|
||||
|
||||
## Checks
|
||||
|
||||
```sh
|
||||
./scripts/ci-check.sh
|
||||
```
|
||||
|
||||
`scripts/test-overview-ui.js` stubs LuCI and renders the widget - a page that
|
||||
merely parses can still die at render.
|
||||
|
||||
## Documentation
|
||||
|
||||
| file | for |
|
||||
| --- | --- |
|
||||
| `docs/MAP.md` | this file |
|
||||
| `docs/DEVELOPMENT.md` | building, signing and releasing |
|
||||
| `README.md` | user-facing, English |
|
||||
| `README.ru.md` | the Russian version |
|
||||
|
Before Width: | Height: | Size: 6.8 KiB |
@@ -1,4 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4Kze8kPGZESezGw8NMN4gDMgaevH
|
||||
+Y9IgZbqWeHpW0gCNXNpctIlS1LSlQraDRwRTJlbg0R+uCGyv0/QnduYkg==
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"luci-app-wrqr": {
|
||||
"description": "Read active Wi-Fi access points for the Status Overview QR widget",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getWirelessDevices" ],
|
||||
"uci": [ "get" ]
|
||||
},
|
||||
"uci": [ "wireless" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require dom';
|
||||
'require rpc';
|
||||
'require uqr';
|
||||
'require wrqr.shared as common';
|
||||
|
||||
var callWirelessConfig = rpc.declare({
|
||||
object: 'uci',
|
||||
method: 'get',
|
||||
params: [ 'config' ],
|
||||
expect: { values: {} }
|
||||
});
|
||||
|
||||
var callWirelessStatus = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getWirelessDevices',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var root = null;
|
||||
var grid = null;
|
||||
var lastFingerprint = null;
|
||||
|
||||
function styles() {
|
||||
return E('style', {}, [
|
||||
'.wrqr-grid{display:flex;flex-wrap:wrap;justify-content:center;align-items:flex-start;gap:2.5rem 1rem;max-width:1164px;margin:0 auto}',
|
||||
'.wrqr-card{display:flex;flex:1 1 190px;flex-direction:column;align-items:center;width:220px;max-width:220px;min-width:0;padding:1rem;box-sizing:border-box;text-align:center}',
|
||||
'.wrqr-ssid{display:block;width:100%;margin:0 0 .25rem;overflow-wrap:anywhere;text-align:center;font-size:1.15rem}',
|
||||
'.wrqr-bands{width:100%;min-height:1.4em;margin:0 0 .8rem;text-align:center;color:var(--text-color-medium,#666);font-size:.85rem}',
|
||||
'.wrqr-code{display:flex;align-items:center;justify-content:center;width:220px;max-width:100%;margin:0 auto;padding:12px;background:#fff;box-sizing:border-box}',
|
||||
'.wrqr-code svg{display:block;width:100%;height:auto;margin:0 auto}',
|
||||
'.wrqr-empty{width:100%;margin:.5rem 0;text-align:center;color:var(--text-color-medium,#666)}',
|
||||
'@media(max-width:700px){.wrqr-grid{gap:2rem 1rem}.wrqr-card{flex-basis:160px;padding:.75rem .25rem}}'
|
||||
]);
|
||||
}
|
||||
|
||||
function card(network) {
|
||||
var svg = uqr.renderSVG(network.payload, {
|
||||
pixelSize: 4,
|
||||
whiteColor: 'white',
|
||||
blackColor: 'black',
|
||||
ecc: 'M',
|
||||
border: 4
|
||||
});
|
||||
svg = svg.replace(/<svg([^>]*) width="([0-9]+)" height="([0-9]+)"/,
|
||||
'<svg$1 viewBox="0 0 $2 $3"');
|
||||
|
||||
return E('div', { 'class': 'wrqr-card' }, [
|
||||
E('strong', { 'class': 'wrqr-ssid' }, [ network.ssid ]),
|
||||
E('div', { 'class': 'wrqr-bands' }, [ network.bands.join(' · ') ]),
|
||||
E('div', { 'class': 'wrqr-code' }, [ E(svg) ])
|
||||
]);
|
||||
}
|
||||
|
||||
function sync(networks) {
|
||||
var nextFingerprint = common.fingerprint(networks);
|
||||
if (nextFingerprint === lastFingerprint)
|
||||
return;
|
||||
|
||||
lastFingerprint = nextFingerprint;
|
||||
dom.content(grid, networks.length ? networks.map(card) : [
|
||||
E('p', { 'class': 'wrqr-empty' }, [ 'No active Wi-Fi access points.' ])
|
||||
]);
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
title: 'Wi-Fi QR',
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callWirelessConfig('wireless'), { values: {} }),
|
||||
L.resolveDefault(callWirelessStatus(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
if (!root) {
|
||||
grid = E('div', { 'class': 'wrqr-grid' });
|
||||
root = E('div', { 'class': 'wrqr-root' }, [ styles(), grid ]);
|
||||
}
|
||||
|
||||
try {
|
||||
sync(common.collect(data && data[0], data && data[1]));
|
||||
}
|
||||
catch (error) {
|
||||
lastFingerprint = null;
|
||||
dom.content(grid, E('p', { 'class': 'wrqr-empty' }, [
|
||||
'Wi-Fi QR unavailable: ' + (error && error.message ? error.message : error)
|
||||
]));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
function enabled(value) {
|
||||
return value !== true && value !== 1 && value !== '1';
|
||||
}
|
||||
|
||||
function percentEncode(value) {
|
||||
var encoded = encodeURIComponent(String(value));
|
||||
var bytes = [];
|
||||
var output = '';
|
||||
var offset;
|
||||
|
||||
for (offset = 0; offset < encoded.length; offset++) {
|
||||
if (encoded.charAt(offset) === '%') {
|
||||
bytes.push(parseInt(encoded.substr(offset + 1, 2), 16));
|
||||
offset += 2;
|
||||
}
|
||||
else {
|
||||
bytes.push(encoded.charCodeAt(offset));
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
var byte = bytes[i];
|
||||
var printable = (byte >= 0x20 && byte <= 0x3a) ||
|
||||
(byte >= 0x3c && byte <= 0x7e);
|
||||
output += printable ? String.fromCharCode(byte) :
|
||||
'%' + byte.toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function security(config) {
|
||||
var encryption = String(config.encryption || 'none').toLowerCase();
|
||||
var key = config.key;
|
||||
|
||||
if (encryption === 'none')
|
||||
return { type: '', key: '', transition: '' };
|
||||
|
||||
if (encryption === 'sae')
|
||||
return key ? { type: 'WPA', key: key, transition: '0' } : null;
|
||||
|
||||
if (encryption === 'sae-mixed' || encryption === 'psk' ||
|
||||
encryption === 'psk2' || encryption === 'psk-mixed' ||
|
||||
encryption === 'psk+psk2')
|
||||
return key ? { type: 'WPA', key: key, transition: '' } : null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function payload(config) {
|
||||
var auth = security(config);
|
||||
var ssid = config.ssid;
|
||||
|
||||
if (!auth || ssid == null || String(ssid) === '')
|
||||
return null;
|
||||
|
||||
return [
|
||||
'WIFI:',
|
||||
auth.type ? 'T:' + auth.type + ';' : '',
|
||||
auth.transition !== '' ? 'R:' + auth.transition + ';' : '',
|
||||
'S:' + percentEncode(ssid) + ';',
|
||||
config.hidden === true || config.hidden === 1 || config.hidden === '1' ?
|
||||
'H:true;' : '',
|
||||
auth.key ? 'P:' + percentEncode(auth.key) + ';' : '',
|
||||
';'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function bandLabel(device) {
|
||||
var band = String((device || {}).band || '').toLowerCase();
|
||||
if (band === '2g') return '2.4 GHz';
|
||||
if (band === '5g') return '5 GHz';
|
||||
if (band === '6g') return '6 GHz';
|
||||
return band ? band.toUpperCase() : '';
|
||||
}
|
||||
|
||||
function activeSections(runtime) {
|
||||
var active = {};
|
||||
Object.keys(runtime || {}).forEach(function(radioName) {
|
||||
var radio = runtime[radioName] || {};
|
||||
if (radio.up === false || radio.disabled === true)
|
||||
return;
|
||||
(radio.interfaces || []).forEach(function(iface) {
|
||||
var section = iface && iface.section;
|
||||
var config = (iface && iface.config) || {};
|
||||
if (section && config.mode === 'ap' && iface.ifname)
|
||||
active[section] = true;
|
||||
});
|
||||
});
|
||||
return active;
|
||||
}
|
||||
|
||||
function collect(configResponse, runtime) {
|
||||
/* rpc.declare({ expect: { values: {} } }) unwraps the `values` member.
|
||||
* Accept the raw ubus shape too so the collector stays easy to test. */
|
||||
var values = (configResponse && configResponse.values) || configResponse || {};
|
||||
var active = activeSections(runtime);
|
||||
var grouped = {};
|
||||
|
||||
Object.keys(values).forEach(function(section) {
|
||||
var config = values[section] || {};
|
||||
var device = values[config.device] || {};
|
||||
var code;
|
||||
|
||||
if (config['.type'] !== 'wifi-iface' || config.mode !== 'ap' ||
|
||||
!enabled(config.disabled) || !enabled(device.disabled) || !active[section])
|
||||
return;
|
||||
|
||||
code = payload(config);
|
||||
if (!code)
|
||||
return;
|
||||
|
||||
if (!grouped[code])
|
||||
grouped[code] = {
|
||||
ssid: String(config.ssid),
|
||||
payload: code,
|
||||
bands: []
|
||||
};
|
||||
|
||||
var band = bandLabel(device);
|
||||
if (band && grouped[code].bands.indexOf(band) < 0)
|
||||
grouped[code].bands.push(band);
|
||||
});
|
||||
|
||||
return Object.keys(grouped).map(function(code) {
|
||||
grouped[code].bands.sort();
|
||||
return grouped[code];
|
||||
}).sort(function(left, right) {
|
||||
return left.ssid < right.ssid ? -1 : left.ssid > right.ssid ? 1 :
|
||||
left.payload < right.payload ? -1 : left.payload > right.payload ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
function fingerprint(networks) {
|
||||
return JSON.stringify(networks.map(function(network) {
|
||||
return [ network.ssid, network.payload, network.bands ];
|
||||
}));
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
percentEncode: percentEncode,
|
||||
payload: payload,
|
||||
collect: collect,
|
||||
fingerprint: fingerprint
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
PKG_NAME=luci-app-wrqr
|
||||
PKG_VERSION=0.1.0
|
||||
PKG_RELEASE=
|
||||
PKG_ARCH=all
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
fail() { printf 'build-apk-release: %s\n' "$*" >&2; exit 1; }
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
. "$root/apk-feed.env"
|
||||
sdk="${OPENWRT_SDK_DIR:-}"
|
||||
secret="${OPENWRT_APK_SIGNING_KEY:-}"
|
||||
public="$root/$OPENWRT_APK_KEY_FILE"
|
||||
[ -d "$sdk" ] || fail 'OPENWRT_SDK_DIR must name an extracted SDK'
|
||||
[ -r "$secret" ] || fail 'OPENWRT_APK_SIGNING_KEY is not readable'
|
||||
[ -r "$public" ] || fail 'shared public key is not readable'
|
||||
"$root/scripts/check-apk-trust.sh"
|
||||
verify_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$verify_dir"' EXIT HUP INT TERM
|
||||
openssl ec -in "$secret" -pubout -out "$verify_dir/public.pem" >/dev/null 2>&1 ||
|
||||
fail 'invalid EC signing key'
|
||||
cmp -s "$verify_dir/public.pem" "$public" || fail 'signing key does not match shared public key'
|
||||
OPENWRT_APK_SIGNING_KEY="$secret" OPENWRT_APK_PUBLIC_KEY="$public" \
|
||||
"$root/scripts/build-apk.sh"
|
||||
apk_tool="$sdk/staging_dir/host/bin/apk"
|
||||
package="$root/dist/$PKG_NAME-$PKG_VERSION.apk"
|
||||
[ -x "$apk_tool" ] || fail 'SDK apk tool is unavailable'
|
||||
[ -r "$package" ] || fail 'built APK is unavailable'
|
||||
"$apk_tool" --allow-untrusted adbsign --sign-key "$secret" "$package"
|
||||
"$apk_tool" --keys-dir "$root/keys" verify "$package"
|
||||
mkdir -p "$root/dist/apk"
|
||||
cp "$package" "$root/dist/apk/"
|
||||
(cd "$root/dist/apk" && sha256sum "$PKG_NAME-$PKG_VERSION.apk" >SHA256SUMS.apk)
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
fail() { printf 'build-apk: %s\n' "$*" >&2; exit 1; }
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
. "$root/apk-feed.env"
|
||||
sdk="${OPENWRT_SDK_DIR:-}"
|
||||
[ -d "$sdk" ] || fail 'OPENWRT_SDK_DIR must name an extracted SDK'
|
||||
apk_tool="$sdk/staging_dir/host/bin/apk"
|
||||
[ -x "$apk_tool" ] || fail 'SDK apk tool is unavailable'
|
||||
"$root/scripts/check-version-sync.sh"
|
||||
work="$(mktemp -d)"
|
||||
cleanup() {
|
||||
if [ -d "$work" ]; then
|
||||
chown -R "$(id -u):$(id -g)" "$work" 2>/dev/null ||
|
||||
sudo -n chown -R "$(id -u):$(id -g)" "$work" 2>/dev/null || true
|
||||
rm -rf "$work"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
"$root/scripts/stage-package.sh" "$work/full"
|
||||
mkdir -p "$work/payload"
|
||||
cp -R "$work/full/usr" "$work/full/www" "$work/payload/"
|
||||
if ! chown -R 0:0 "$work/payload" 2>/dev/null; then
|
||||
command -v sudo >/dev/null 2>&1 || fail 'root-owned APK staging requires chown or sudo'
|
||||
sudo -n chown -R 0:0 "$work/payload" || fail 'could not normalize APK ownership'
|
||||
fi
|
||||
mkdir -p "$root/dist"
|
||||
artifact="$root/dist/$PKG_NAME-$PKG_VERSION.apk"
|
||||
"$apk_tool" mkpkg \
|
||||
--info "name:$PKG_NAME" \
|
||||
--info "version:$PKG_VERSION" \
|
||||
--info 'description:Active Wi-Fi access point QR codes for LuCI Status Overview' \
|
||||
--info "arch:$OPENWRT_APK_ARCH" \
|
||||
--info 'license:MIT' \
|
||||
--info "origin:$PKG_NAME" \
|
||||
--info 'maintainer:nikitid' \
|
||||
--info 'depends:libc luci-base luci-lib-uqr' \
|
||||
--script "post-install:$work/full/CONTROL/postinst" \
|
||||
--script "post-upgrade:$work/full/CONTROL/postinst" \
|
||||
--script "pre-deinstall:$work/full/CONTROL/prerm" \
|
||||
--files "$work/payload" \
|
||||
--output "$artifact"
|
||||
"$apk_tool" adbdump --format json "$artifact" >"$work/package.json"
|
||||
python3 - "$work/package.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], encoding="utf-8") as source:
|
||||
package = json.load(source)
|
||||
if package.get("info", {}).get("depends") != ["libc", "luci-base", "luci-lib-uqr"]:
|
||||
raise SystemExit("unexpected APK dependency set")
|
||||
for path in package.get("paths", []):
|
||||
entries = [path] + path.get("files", [])
|
||||
for entry in entries:
|
||||
acl = entry.get("acl", {})
|
||||
if acl.get("user") != "root" or acl.get("group") != "root":
|
||||
raise SystemExit("APK contains a non-root-owned path")
|
||||
if any(path.get("name") == "CONTROL" for path in package.get("paths", [])):
|
||||
raise SystemExit("IPK control directory leaked into APK payload")
|
||||
scripts = package.get("scripts", {})
|
||||
if set(scripts) != {"post-install", "post-upgrade", "pre-deinstall"}:
|
||||
raise SystemExit("unexpected APK lifecycle scripts")
|
||||
PY
|
||||
printf '%s\n' "$artifact"
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
stage="${BUILD_DIR:-$root/build/ipk}/stage"
|
||||
mkdir -p "$root/dist"
|
||||
"$root/scripts/check-version-sync.sh"
|
||||
"$root/scripts/stage-package.sh" "$stage"
|
||||
rm -f "$root/dist"/*.ipk "$root/dist/SHA256SUMS"
|
||||
python3 "$root/scripts/pack-ipk.py" "$stage" "$root/dist"
|
||||
(cd "$root/dist" && sha256sum ./*.ipk >SHA256SUMS)
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/apk-feed.env"
|
||||
actual="$(sha256sum "$root/$OPENWRT_APK_KEY_FILE" | awk '{print $1}')"
|
||||
[ "$actual" = "$OPENWRT_APK_TRUST_SHA256" ] || {
|
||||
printf 'shared APK public key checksum mismatch\n' >&2
|
||||
exit 1
|
||||
}
|
||||
if grep -R -E '(apk|opkg)[[:space:]]+upgrade([[:space:]]*$|[[:space:]]*(&&|;))' \
|
||||
"$root/scripts" "$root/README.md" >/dev/null 2>&1; then
|
||||
printf 'unscoped package upgrade found\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# template: check-readme v1 (repo-templates)
|
||||
# Formatted to pass both `shfmt -i 2` and `shfmt -i 2 -bn -ci`, the styles
|
||||
# the consuming repositories check with; each gets this file verbatim.
|
||||
# Do not edit in place: change templates/shared/ in repo-templates
|
||||
# and run scripts/sync-templates.sh --update.
|
||||
#
|
||||
# Hold README.md (English) and README.ru.md (Russian) to the shared layout:
|
||||
#
|
||||
# # <Product> for <platform> # <Product> для <platform>
|
||||
# [Русский](README.ru.md) [English](README.md)
|
||||
# badges: CI, Release, License
|
||||
# ## Features ## Возможности
|
||||
# ## Requirements ## Требования
|
||||
# ## Installation ## Установка
|
||||
# ## <project sections> ## <project sections>
|
||||
# ## Development ## Разработка
|
||||
# ## Documentation ## Документация
|
||||
# ## Support ## Поддержка
|
||||
# ## License ## Лицензия
|
||||
#
|
||||
# Both files must have the same number of sections, so one language cannot
|
||||
# silently drop what the other says. Punctuation is plain ASCII in both: no
|
||||
# typographic dashes, arrows, quotes or ellipses. The release badge is required
|
||||
# only where .github/workflows/release.yml exists. Every relative link must
|
||||
# resolve.
|
||||
|
||||
set -eu
|
||||
|
||||
root="$(CDPATH='' cd -- "$(dirname "$0")/.." && pwd)"
|
||||
cd "$root"
|
||||
|
||||
status=0
|
||||
fail() {
|
||||
printf 'README: %s\n' "$*" >&2
|
||||
status=1
|
||||
}
|
||||
|
||||
headings() {
|
||||
sed -n 's/^## //p' "$1"
|
||||
}
|
||||
|
||||
# check_layout <file> <first three sections> <last four sections>, each list
|
||||
# separated by '|'. Project sections may only sit between the two.
|
||||
check_layout() {
|
||||
file="$1"
|
||||
head_want="$2"
|
||||
tail_want="$3"
|
||||
if [ ! -f "$file" ]; then
|
||||
fail "$file is missing"
|
||||
return
|
||||
fi
|
||||
if ! head -n 1 "$file" | grep -q '^# '; then
|
||||
fail "$file does not start with a '# ' title"
|
||||
fi
|
||||
head_have="$(headings "$file" | head -n 3 | paste -sd '|' -)"
|
||||
tail_have="$(headings "$file" | tail -n 4 | paste -sd '|' -)"
|
||||
if [ "$head_have" != "$head_want" ]; then
|
||||
fail "$file: the first sections are '$head_have', expected '$head_want'"
|
||||
fi
|
||||
if [ "$tail_have" != "$tail_want" ]; then
|
||||
fail "$file: the last sections are '$tail_have', expected '$tail_want'"
|
||||
fi
|
||||
}
|
||||
|
||||
check_layout README.md 'Features|Requirements|Installation' \
|
||||
'Development|Documentation|Support|License'
|
||||
check_layout README.ru.md 'Возможности|Требования|Установка' \
|
||||
'Разработка|Документация|Поддержка|Лицензия'
|
||||
|
||||
if ! grep -qF '[Русский](README.ru.md)' README.md; then
|
||||
fail 'README.md does not link to README.ru.md'
|
||||
fi
|
||||
if ! grep -qF '[English](README.md)' README.ru.md; then
|
||||
fail 'README.ru.md does not link to README.md'
|
||||
fi
|
||||
|
||||
en="$(headings README.md | wc -l | tr -d ' ')"
|
||||
ru="$(headings README.ru.md | wc -l | tr -d ' ')"
|
||||
if [ "$en" != "$ru" ]; then
|
||||
fail "README.md has $en sections and README.ru.md has $ru"
|
||||
fi
|
||||
|
||||
for file in README.md README.ru.md; do
|
||||
if ! grep -q 'actions/workflows/.*badge\.svg' "$file"; then
|
||||
fail "$file has no CI badge"
|
||||
fi
|
||||
if ! grep -q 'img\.shields\.io/github/license\|License-\|license-' "$file"; then
|
||||
fail "$file has no license badge"
|
||||
fi
|
||||
if [ -f .github/workflows/release.yml ]; then
|
||||
if ! grep -q 'img\.shields\.io/github/v/release' "$file"; then
|
||||
fail "$file has no release badge"
|
||||
fi
|
||||
fi
|
||||
|
||||
# The curly quotes in the pattern are what it looks for, not a typo.
|
||||
# shellcheck disable=SC1112
|
||||
typographic="$(grep -n '—\|–\|→\|←\|«\|»\|“\|”\|‘\|’\|…' "$file" | head -n 3 || true)"
|
||||
if [ -n "$typographic" ]; then
|
||||
fail "$file has non-ASCII punctuation: $typographic"
|
||||
fi
|
||||
|
||||
# Relative links: [text](path) and [text](path#anchor), not URLs or anchors.
|
||||
links="$(grep -o '](\([^)#:]*\)[)#]' "$file" | sed 's/^](//; s/[)#]$//' | sort -u)"
|
||||
missing=""
|
||||
for target in $links; do
|
||||
[ -e "$target" ] || missing="$missing $target"
|
||||
done
|
||||
[ -z "$missing" ] || fail "$file links to missing files:$missing"
|
||||
done
|
||||
|
||||
[ "$status" -eq 0 ] && printf 'README layout OK: %s sections each\n' "$en"
|
||||
exit "$status"
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
[ "${1:?usage: check-release-tag.sh TAG}" = "v$PKG_VERSION" ] || {
|
||||
printf 'release tag must be v%s\n' "$PKG_VERSION" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
make_name="$(sed -n 's/^PKG_NAME:=//p' "$root/Makefile" | head -n 1)"
|
||||
make_version="$(sed -n 's/^PKG_VERSION:=//p' "$root/Makefile" | head -n 1)"
|
||||
make_arch="$(sed -n 's/^PKGARCH:=//p' "$root/Makefile" | head -n 1)"
|
||||
[ "$make_name" = "$PKG_NAME" ]
|
||||
[ "$make_version" = "$PKG_VERSION" ]
|
||||
[ "$make_arch" = "$PKG_ARCH" ]
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
cd "$root"
|
||||
./scripts/check-version-sync.sh
|
||||
./scripts/check-readme.sh
|
||||
./scripts/check-apk-trust.sh
|
||||
find scripts -type f -name '*.sh' -exec sh -n {} +
|
||||
find luci -type f -name '*.js' -exec node --check {} +
|
||||
node ./scripts/test-shared.js
|
||||
node ./scripts/test-overview-ui.js
|
||||
python3 -m py_compile scripts/pack-ipk.py
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
for path in Path('luci').glob('*.json'):
|
||||
json.loads(path.read_text())
|
||||
print('json OK:', path)
|
||||
PY
|
||||
./scripts/build-ipk.sh
|
||||
if find "${BUILD_DIR:-$root/build/ipk}/stage" -type f -name '._*' | grep -q .; then
|
||||
printf 'AppleDouble metadata leaked into staged package\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
first="$(sha256sum dist/*.ipk | awk '{print $1}')"
|
||||
./scripts/build-ipk.sh
|
||||
second="$(sha256sum dist/*.ipk | awk '{print $1}')"
|
||||
[ "$first" = "$second" ]
|
||||
git diff --check 2>/dev/null || true
|
||||
printf 'ci-check OK\n'
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic opkg package."""
|
||||
import gzip
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
|
||||
def normalize(info):
|
||||
info.uid = info.gid = info.mtime = 0
|
||||
info.uname = info.gname = "root"
|
||||
return info
|
||||
|
||||
|
||||
def archive(members):
|
||||
raw = io.BytesIO()
|
||||
with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as out:
|
||||
for name, path in members:
|
||||
info = normalize(out.gettarinfo(path, name))
|
||||
if info.isreg():
|
||||
with open(path, "rb") as source:
|
||||
out.addfile(info, source)
|
||||
else:
|
||||
out.addfile(info)
|
||||
compressed = io.BytesIO()
|
||||
with gzip.GzipFile(fileobj=compressed, mode="wb", mtime=0) as out:
|
||||
out.write(raw.getvalue())
|
||||
return compressed.getvalue()
|
||||
|
||||
|
||||
def walk(stage, control=False):
|
||||
result = []
|
||||
base = os.path.join(stage, "CONTROL") if control else stage
|
||||
for root, dirs, files in os.walk(base):
|
||||
dirs[:] = sorted(d for d in dirs if control or d != "CONTROL")
|
||||
rel = os.path.relpath(root, base)
|
||||
if rel != ".":
|
||||
result.append(("./" + rel, root))
|
||||
for name in sorted(files):
|
||||
result.append(("./" + os.path.join(rel, name).replace("./", "", 1),
|
||||
os.path.join(root, name)))
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def control_field(path, field):
|
||||
with open(path, encoding="utf-8") as source:
|
||||
for line in source:
|
||||
if line.startswith(field + ":"):
|
||||
return line.split(":", 1)[1].strip()
|
||||
raise SystemExit("missing control field: " + field)
|
||||
|
||||
|
||||
stage, output = sys.argv[1:]
|
||||
control = os.path.join(stage, "CONTROL", "control")
|
||||
parts = {
|
||||
"debian-binary": b"2.0\n",
|
||||
"control.tar.gz": archive(walk(stage, True)),
|
||||
"data.tar.gz": archive(walk(stage, False)),
|
||||
}
|
||||
os.makedirs(output, exist_ok=True)
|
||||
name = "%s_%s_%s.ipk" % tuple(control_field(control, field) for field in
|
||||
("Package", "Version", "Architecture"))
|
||||
target = os.path.join(output, name)
|
||||
raw = io.BytesIO()
|
||||
with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as out:
|
||||
for part in ("debian-binary", "control.tar.gz", "data.tar.gz"):
|
||||
data = parts[part]
|
||||
info = normalize(tarfile.TarInfo("./" + part))
|
||||
info.mode, info.size = 0o644, len(data)
|
||||
out.addfile(info, io.BytesIO(data))
|
||||
with gzip.GzipFile(target, "wb", mtime=0) as out:
|
||||
out.write(raw.getvalue())
|
||||
print(target)
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# template: release-notes v3 (repo-templates)
|
||||
# Formatted to pass both `shfmt -i 2` and `shfmt -i 2 -bn -ci`, the styles
|
||||
# the consuming repositories check with; each gets this file verbatim.
|
||||
# Do not edit in place: change templates/shared/ in repo-templates
|
||||
# and run scripts/sync-templates.sh --update.
|
||||
#
|
||||
# Print release notes for a tag, built from the subjects of the commits since
|
||||
# the previous release tag. There is no changelog file: the commit subject is
|
||||
# the release note, so write it as the line a user should read.
|
||||
#
|
||||
# A commit whose subject is only a version number ("Release 1.2.0",
|
||||
# "release: v0.2.0") says nothing a user can read and is left out.
|
||||
#
|
||||
# A commit that touches only documentation, CI, tests or checks is listed
|
||||
# under "Documentation and tooling" rather than "Changes".
|
||||
#
|
||||
# Usage: scripts/release-notes.sh <tag> [previous-tag]
|
||||
#
|
||||
# The previous tag defaults to the nearest v* tag before <tag>. For a final
|
||||
# release, pre-release tags (_rc, _beta, _alpha) are skipped, so its notes
|
||||
# cover everything since the last final release rather than since its RC. The repository
|
||||
# for the compare link comes from GITHUB_REPOSITORY, or from the origin remote.
|
||||
# Needs the full history: check out with fetch-depth 0.
|
||||
|
||||
set -eu
|
||||
|
||||
[ $# -ge 1 ] || {
|
||||
printf 'usage: %s <tag> [previous-tag]\n' "$0" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
tag="$1"
|
||||
if [ $# -ge 2 ]; then
|
||||
previous="$2"
|
||||
elif printf '%s\n' "$tag" | grep -Eq '_(rc|beta|alpha)'; then
|
||||
previous="$(git describe --tags --abbrev=0 --match 'v*' "$tag^" 2>/dev/null || true)"
|
||||
else
|
||||
previous="$(git describe --tags --abbrev=0 --match 'v*' --exclude '*_rc*' \
|
||||
--exclude '*_beta*' --exclude '*_alpha*' "$tag^" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
repository="${GITHUB_REPOSITORY:-}"
|
||||
if [ -z "$repository" ]; then
|
||||
url="$(git remote get-url origin 2>/dev/null || true)"
|
||||
url="${url%.git}"
|
||||
repository="$(printf '%s\n' "$url" | sed -n 's#.*github\.com[:/]\(.*\)$#\1#p')"
|
||||
fi
|
||||
|
||||
if [ -n "$previous" ]; then
|
||||
range="$previous..$tag"
|
||||
else
|
||||
range="$tag"
|
||||
fi
|
||||
|
||||
# Subjects that carry nothing but a version number.
|
||||
bump='^(release|bump|version)[: ]+(to )?v?[0-9]+(\.[0-9]+)*([-_][a-z0-9]+)?$'
|
||||
|
||||
# Paths that never change what a user runs.
|
||||
tooling='^(docs/|\.github/|tests?/|scripts/(check|test|gen|ci)-|scripts/release-notes\.sh$|LICENSE|NOTICE|THIRD_PARTY|.*\.md$|\.[^/]+$)'
|
||||
|
||||
changes=""
|
||||
tooling_changes=""
|
||||
for commit in $(git rev-list --no-merges --reverse "$range"); do
|
||||
subject="$(git log -1 --format=%s "$commit")"
|
||||
if printf '%s\n' "$subject" | grep -Eiq "$bump"; then
|
||||
continue
|
||||
fi
|
||||
paths="$(git diff-tree --no-commit-id --name-only -r --root "$commit")"
|
||||
if printf '%s\n' "$paths" | grep -Evq "$tooling"; then
|
||||
changes="$changes- $subject
|
||||
"
|
||||
else
|
||||
tooling_changes="$tooling_changes- $subject
|
||||
"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$changes" ]; then
|
||||
printf '## Changes\n\n%s' "$changes"
|
||||
fi
|
||||
if [ -n "$tooling_changes" ]; then
|
||||
[ -z "$changes" ] || printf '\n'
|
||||
printf '## Documentation and tooling\n\n%s' "$tooling_changes"
|
||||
fi
|
||||
if [ -z "$changes$tooling_changes" ]; then
|
||||
printf 'Version update only; no other changes since %s.\n' "$previous"
|
||||
fi
|
||||
|
||||
if [ -n "$repository" ]; then
|
||||
if [ -n "$previous" ]; then
|
||||
printf '\n**Full diff**: https://github.com/%s/compare/%s...%s\n' \
|
||||
"$repository" "$previous" "$tag"
|
||||
else
|
||||
printf '\n**Full history**: https://github.com/%s/commits/%s\n' \
|
||||
"$repository" "$tag"
|
||||
fi
|
||||
fi
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
stage="${1:?usage: stage-package.sh STAGE_DIR}"
|
||||
. "$root/release.env"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage/CONTROL" "$stage/usr/share/rpcd/acl.d" \
|
||||
"$stage/usr/share/licenses/$PKG_NAME" \
|
||||
"$stage/www/luci-static/resources/wrqr" \
|
||||
"$stage/www/luci-static/resources/view/status/include"
|
||||
install -m 644 "$root/luci/acl.json" "$stage/usr/share/rpcd/acl.d/$PKG_NAME.json"
|
||||
install -m 644 "$root/luci/shared.js" "$stage/www/luci-static/resources/wrqr/shared.js"
|
||||
install -m 644 "$root/luci/overview.js" "$stage/www/luci-static/resources/view/status/include/15_wifi-qr.js"
|
||||
install -m 644 "$root/LICENSE" "$stage/usr/share/licenses/$PKG_NAME/LICENSE"
|
||||
cat >"$stage/CONTROL/control" <<EOF
|
||||
Package: $PKG_NAME
|
||||
Version: $PKG_VERSION
|
||||
Architecture: $PKG_ARCH
|
||||
Maintainer: nikitid
|
||||
Depends: luci-base, luci-lib-uqr
|
||||
Section: luci
|
||||
Priority: optional
|
||||
Description: Display active Wi-Fi access point QR codes on LuCI Status Overview.
|
||||
EOF
|
||||
cat >"$stage/CONTROL/postinst" <<'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT:-}" ] && exit 0
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache
|
||||
exit 0
|
||||
EOF
|
||||
cat >"$stage/CONTROL/prerm" <<'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT:-}" ] && exit 0
|
||||
case "${1:-}" in upgrade) exit 0 ;; esac
|
||||
[ "${PKG_UPGRADE:-0}" = 1 ] && exit 0
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$stage/CONTROL/postinst" "$stage/CONTROL/prerm"
|
||||
@@ -1,74 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
let replacements = 0;
|
||||
let qrRenders = 0;
|
||||
|
||||
function node(tag, attrs, children) {
|
||||
return {
|
||||
tag, attrs: attrs || {}, children: children || []
|
||||
};
|
||||
}
|
||||
|
||||
function evaluate(path, context) {
|
||||
return vm.runInNewContext(`(function(){${fs.readFileSync(path, 'utf8')}\n})()`, context);
|
||||
}
|
||||
|
||||
const common = evaluate('luci/shared.js', {
|
||||
baseclass: { extend: value => value }, encodeURIComponent
|
||||
});
|
||||
const rpcCalls = [];
|
||||
const context = {
|
||||
baseclass: { extend: value => value },
|
||||
common,
|
||||
dom: { content: (target, next) => { replacements++; target.children = next; } },
|
||||
uqr: { renderSVG: value => {
|
||||
qrRenders++;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="148" height="148">${value}</svg>`;
|
||||
} },
|
||||
rpc: { declare: spec => (...args) => { rpcCalls.push([spec.object, spec.method, args]); return Promise.resolve({}); } },
|
||||
L: { resolveDefault: promise => promise },
|
||||
Promise,
|
||||
E: (tag, attrs, children) => node(tag, attrs, children)
|
||||
};
|
||||
const widget = evaluate('luci/overview.js', context);
|
||||
|
||||
const config = { values: {
|
||||
radio0: { '.type': 'wifi-device', band: '5g' },
|
||||
ap0: { '.type': 'wifi-iface', device: 'radio0', mode: 'ap', ssid: 'Home', encryption: 'sae-mixed', key: 'secret' }
|
||||
} };
|
||||
const running = { radio0: { up: true, interfaces: [
|
||||
{ section: 'ap0', ifname: 'phy0-ap0', config: { mode: 'ap' } }
|
||||
] } };
|
||||
|
||||
const firstRoot = widget.render([ config, running ]);
|
||||
if (replacements !== 1 || qrRenders !== 1)
|
||||
throw new Error('first render did not create exactly one QR snapshot');
|
||||
const qrMarkup = firstRoot.children[1].children[0].children[2].children[0].tag;
|
||||
if (!qrMarkup.includes('viewBox="0 0 148 148"') || qrMarkup.includes(' width="148"'))
|
||||
throw new Error('QR SVG was not converted to a scalable viewBox');
|
||||
|
||||
replacements = 0;
|
||||
qrRenders = 0;
|
||||
const secondRoot = widget.render([ config, running ]);
|
||||
if (secondRoot !== firstRoot)
|
||||
throw new Error('polling replaced the stable widget root');
|
||||
if (replacements !== 0 || qrRenders !== 0)
|
||||
throw new Error('unchanged poll mutated or regenerated the widget');
|
||||
|
||||
const stopped = { radio0: { up: false, interfaces: [] } };
|
||||
widget.render([ config, stopped ]);
|
||||
if (replacements !== 1)
|
||||
throw new Error('runtime change was not rendered');
|
||||
|
||||
widget.load().then(() => {
|
||||
if (JSON.stringify(rpcCalls.map(call => call.slice(0, 2))) !==
|
||||
JSON.stringify([ [ 'uci', 'get' ], [ 'luci-rpc', 'getWirelessDevices' ] ]))
|
||||
throw new Error('load did not refresh configuration and runtime state');
|
||||
console.log('overview UI tests OK');
|
||||
}).catch(error => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
function load(path) {
|
||||
return vm.runInNewContext(`(function(){${fs.readFileSync(path, 'utf8')}\n})()`, {
|
||||
baseclass: { extend: value => value },
|
||||
encodeURIComponent
|
||||
});
|
||||
}
|
||||
|
||||
const common = load('luci/shared.js');
|
||||
|
||||
function equal(actual, expected, label) {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected))
|
||||
throw new Error(`${label}: ${JSON.stringify(actual)} != ${JSON.stringify(expected)}`);
|
||||
}
|
||||
|
||||
equal(common.percentEncode('a;b\nя'), 'a%3Bb%0A%D1%8F', 'percent encoding');
|
||||
equal(common.payload({ ssid: 'Guest', encryption: 'none' }),
|
||||
'WIFI:S:Guest;;', 'open network');
|
||||
equal(common.payload({ ssid: 'Main', encryption: 'sae-mixed', key: 'secret' }),
|
||||
'WIFI:T:WPA;S:Main;P:secret;;', 'transition network');
|
||||
equal(common.payload({ ssid: 'Main', encryption: 'sae', key: 'secret', hidden: '1' }),
|
||||
'WIFI:T:WPA;R:0;S:Main;H:true;P:secret;;', 'WPA3 network');
|
||||
equal(common.payload({ ssid: 'Corp', encryption: 'wpa2', key: 'secret' }), null,
|
||||
'enterprise network');
|
||||
|
||||
const config = { values: {
|
||||
radio0: { '.type': 'wifi-device', band: '2g' },
|
||||
radio1: { '.type': 'wifi-device', band: '5g' },
|
||||
radio2: { '.type': 'wifi-device', band: '6g', disabled: '1' },
|
||||
ap0: { '.type': 'wifi-iface', device: 'radio0', mode: 'ap', ssid: 'Home', encryption: 'psk2', key: 'same' },
|
||||
ap1: { '.type': 'wifi-iface', device: 'radio1', mode: 'ap', ssid: 'Home', encryption: 'sae-mixed', key: 'same' },
|
||||
ap2: { '.type': 'wifi-iface', device: 'radio2', mode: 'ap', ssid: 'Off', encryption: 'sae', key: 'off' },
|
||||
sta0: { '.type': 'wifi-iface', device: 'radio1', mode: 'sta', ssid: 'Upstream', encryption: 'psk2', key: 'skip' },
|
||||
broken: { '.type': 'wifi-iface', device: 'radio1', mode: 'ap', ssid: 'Broken', encryption: 'psk2', key: 'skip' }
|
||||
} };
|
||||
const runtime = {
|
||||
radio0: { up: true, interfaces: [ { section: 'ap0', ifname: 'phy0-ap0', config: { mode: 'ap' } } ] },
|
||||
radio1: { up: true, interfaces: [
|
||||
{ section: 'ap1', ifname: 'phy1-ap0', config: { mode: 'ap' } },
|
||||
{ section: 'sta0', ifname: 'phy1-sta0', config: { mode: 'sta' } }
|
||||
] },
|
||||
radio2: { up: false, interfaces: [ { section: 'ap2', ifname: 'phy2-ap0', config: { mode: 'ap' } } ] }
|
||||
};
|
||||
|
||||
equal(common.collect(config, runtime), [ {
|
||||
ssid: 'Home',
|
||||
payload: 'WIFI:T:WPA;S:Home;P:same;;',
|
||||
bands: [ '2.4 GHz', '5 GHz' ]
|
||||
} ], 'active AP collection and deduplication');
|
||||
equal(common.collect(config.values, runtime).length, 1, 'RPC-unwrapped UCI response');
|
||||
|
||||
console.log('shared tests OK');
|
||||