diff --git a/net/adblock-fast/LICENSE b/net/adblock-fast/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/net/adblock-fast/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/net/adblock-fast/Makefile b/net/adblock-fast/Makefile index e1bd93dc97..a8a1acb69d 100644 --- a/net/adblock-fast/Makefile +++ b/net/adblock-fast/Makefile @@ -4,8 +4,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=adblock-fast -PKG_VERSION:=1.2.1 -PKG_RELEASE:=7 +PKG_VERSION:=1.2.2 +PKG_RELEASE:=6 PKG_MAINTAINER:=Stan Grishin PKG_LICENSE:=AGPL-3.0-or-later @@ -15,12 +15,15 @@ define Package/adblock-fast SECTION:=net CATEGORY:=Network TITLE:=AdBlock Fast Service - URL:=https://github.com/stangri/adblock-fast/ + URL:=https://github.com/mossdef-org/adblock-fast/ PKGARCH:=all DEPENDS:= \ - +jshn \ +curl \ +resolveip \ + +ucode \ + +ucode-mod-fs \ + +ucode-mod-uci \ + +ucode-mod-ubus \ +!BUSYBOX_DEFAULT_AWK:gawk \ +!BUSYBOX_DEFAULT_GREP:grep \ +!BUSYBOX_DEFAULT_SED:sed \ @@ -46,31 +49,21 @@ endef define Package/adblock-fast/install $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_BIN) ./files/etc/init.d/adblock-fast $(1)/etc/init.d/adblock-fast - $(SED) "s|^\(readonly PKG_VERSION\).*|\1='$(PKG_VERSION)-r$(PKG_RELEASE)'|" $(1)/etc/init.d/adblock-fast + $(INSTALL_DIR) $(1)/lib/adblock-fast + $(INSTALL_DATA) ./files/lib/adblock-fast/adblock-fast.uc $(1)/lib/adblock-fast/adblock-fast.uc + $(INSTALL_DATA) ./files/lib/adblock-fast/cli.uc $(1)/lib/adblock-fast/cli.uc + $(SED) "s|^\(\tversion:\).*|\1 '$(PKG_VERSION)-r$(PKG_RELEASE)',|" $(1)/lib/adblock-fast/adblock-fast.uc $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/etc/config/adblock-fast $(1)/etc/config/adblock-fast $(INSTALL_DIR) $(1)/etc/uci-defaults $(INSTALL_BIN) ./files/etc/uci-defaults/90-adblock-fast $(1)/etc/uci-defaults/90-adblock-fast endef -define Package/adblock-fast/postinst -#!/bin/sh -# check if we are on real system -if [ -z "$${IPKG_INSTROOT}" ]; then - /etc/init.d/adblock-fast enable -fi -exit 0 -endef - define Package/adblock-fast/prerm #!/bin/sh -# check if we are on real system if [ -z "$${IPKG_INSTROOT}" ]; then - echo -n "Stopping adblock-fast service... " - { /etc/init.d/adblock-fast stop && \ - /etc/init.d/adblock-fast killcache; } >/dev/null 2>&1 && echo "OK" || echo "FAIL" - echo -n "Removing rc.d symlink for adblock-fast... " - /etc/init.d/adblock-fast disable >/dev/null 2>&1 && echo "OK" || echo "FAIL" + echo -n "Removing adblock-fast cache... " + /etc/init.d/adblock-fast killcache >/dev/null 2>&1 && echo "OK" || echo "FAIL" fi exit 0 endef diff --git a/net/adblock-fast/files/etc/init.d/adblock-fast b/net/adblock-fast/files/etc/init.d/adblock-fast old mode 100755 new mode 100644 index 73d53153fb..5971da1299 --- a/net/adblock-fast/files/etc/init.d/adblock-fast +++ b/net/adblock-fast/files/etc/init.d/adblock-fast @@ -1,10 +1,9 @@ #!/bin/sh /etc/rc.common -# Copyright 2023-2025 MOSSDeF, Stan Grishin (stangri@melmac.ca) -# shellcheck disable=SC2015,SC3023,SC3043 +# Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca) +# shellcheck disable=SC2015,SC3043 # shellcheck disable=SC2034 START=20 -# shellcheck disable=SC2034 USE_PROCD=1 LC_ALL=C @@ -23,2678 +22,65 @@ if type extra_command 1>/dev/null 2>&1; then fi readonly packageName='adblock-fast' -readonly PKG_VERSION='dev-test' -readonly packageCompat='11' -readonly serviceName="$packageName $PKG_VERSION" -readonly packageMemoryThreshold='33554432' -readonly packageConfigFile="/etc/config/${packageName}" -readonly dnsmasqUnifiedFile="/var/run/${packageName}/${packageName}.dnsmasq" -readonly dnsmasqAddnhostsFile="/var/run/${packageName}/dnsmasq.addnhosts" -readonly dnsmasqAddnhostsCache="/var/run/${packageName}/dnsmasq.addnhosts.cache" -readonly dnsmasqAddnhostsGzip="${packageName}.dnsmasq.addnhosts.gz" -readonly dnsmasqAddnhostsOutputFormatFilter='s|^|127.0.0.1 |;s|$||' -readonly dnsmasqAddnhostsOutputFormatFilterIPv6='s|^|:: |;s|$||' -readonly dnsmasqAddnhostsOutputParseFilter='s|^127.0.0.1 ||;s|^:: ||;' -readonly dnsmasqAddnhostsGrepPatternIPv4='s|^|^127\.0\.0\.1 |' -readonly dnsmasqAddnhostsGrepPatternIPv6='s|^|^:: |' -readonly dnsmasqConfFile="$dnsmasqUnifiedFile" -readonly dnsmasqConfCache="/var/run/${packageName}/dnsmasq.conf.cache" -readonly dnsmasqConfGzip="${packageName}.dnsmasq.conf.gz" -readonly dnsmasqConfOutputFormatFilter='s|^|local=/|;s|$|/|' -readonly dnsmasqConfOutputParseFilter='s|local=/||;s|/$||;' -readonly dnsmasqConfGrepPattern='s|^|^local=/|;s|$|/$|' -readonly dnsmasqIpsetFile="$dnsmasqUnifiedFile" -readonly dnsmasqIpsetCache="/var/run/${packageName}/dnsmasq.ipset.cache" -readonly dnsmasqIpsetGzip="${packageName}.dnsmasq.ipset.gz" -readonly dnsmasqIpsetOutputFormatFilter='s|^|ipset=/|;s|$|/adb|' -readonly dnsmasqIpsetOutputParseFilter='s|ipset=/||;s|/adb$||;' -readonly dnsmasqIpsetGrepPattern='s|^|^ipset=/|;s|$|/adb$|' -readonly dnsmasqNftsetFile="$dnsmasqUnifiedFile" -readonly dnsmasqNftsetCache="/var/run/${packageName}/dnsmasq.nftset.cache" -readonly dnsmasqNftsetGzip="${packageName}.dnsmasq.nftset.gz" -readonly dnsmasqNftsetOutputFormatFilter='s|^|nftset=/|;s|$|/4#inet#fw4#adb4|' -readonly dnsmasqNftsetOutputFormatFilterIPv6='s|^|nftset=/|;s|$|/4#inet#fw4#adb4,6#inet#fw4#adb6|' -readonly dnsmasqNftsetOutputParseFilter='s|nftset=/||;s|/4#.*$||;' -readonly dnsmasqNftsetGrepPattern='s|^|^nftset=/|;s|$|/4#.*$|' -readonly dnsmasqServersFile="/var/run/${packageName}/dnsmasq.servers" -readonly dnsmasqServersCache="/var/run/${packageName}/dnsmasq.servers.cache" -readonly dnsmasqServersGzip="${packageName}.dnsmasq.servers.gz" -readonly dnsmasqServersOutputFormatFilter='s|^|server=/|;s|$|/|' -readonly dnsmasqServersAllowFilter='s|(.*)|server=/\1/#|' -readonly dnsmasqServersBlockedCountFilter='\|/#|d' -readonly dnsmasqServersOutputParseFilter='s|server=/||;s|/.*$||;' -readonly dnsmasqServersGrepPattern='s|^|^server=/|;s|$|/$|' -readonly smartdnsDomainSetFile="/var/run/${packageName}/smartdns.domainset" -readonly smartdnsDomainSetCache="/var/run/${packageName}/smartdns.domainset.cache" -readonly smartdnsDomainSetConfig="/var/run/${packageName}/smartdns.domainset.conf" -readonly smartdnsDomainSetGzip="${packageName}.smartdns.domainset.gz" -readonly smartdnsDomainSetOutputFormatFilter='' -readonly smartdnsDomainSetOutputParseFilter='' -readonly smartdnsIpsetFile="/var/run/${packageName}/smartdns.ipset" -readonly smartdnsIpsetCache="/var/run/${packageName}/smartdns.ipset.cache" -readonly smartdnsIpsetConfig="/var/run/${packageName}/smartdns.ipset.conf" -readonly smartdnsIpsetGzip="${packageName}.smartdns.ipset.gz" -readonly smartdnsIpsetOutputFormatFilter='' -readonly smartdnsIpsetOutputParseFilter='' -readonly smartdnsNftsetFile="/var/run/${packageName}/smartdns.nftset" -readonly smartdnsNftsetCache="/var/run/${packageName}/smartdns.nftset.cache" -readonly smartdnsNftsetConfig="/var/run/${packageName}/smartdns.nftset.conf" -readonly smartdnsNftsetGzip="${packageName}.smartdns.nftset.gz" -readonly smartdnsNftsetOutputFormatFilter='' -readonly smartdnsNftsetOutputParseFilter='' -readonly unboundFile="/var/lib/unbound/adb_list.${packageName}" -readonly unboundCache="/var/run/${packageName}/unbound.cache" -readonly unboundGzip="${packageName}.unbound.gz" -readonly unboundOutputFormatFilter='s|^|local-zone: "|;s|$|." always_nxdomain|' -readonly unboundOutputParseFilter='s|^local-zone: "||;s|." always_nxdomain$||;' - -readonly ALLOWED_TMP="/var/${packageName}.allowed.tmp" -readonly A_TMP="/var/${packageName}.a.tmp" -readonly B_TMP="/var/${packageName}.b.tmp" -readonly SED_TMP="/var/${packageName}.sed.tmp" -readonly uciConfigFile="/etc/config/${packageName}" -readonly runningConfigFile="/dev/shm/${packageName}" -readonly runningStatusFile="/dev/shm/${packageName}.status.json" -readonly runningStatusFileLock="/var/lock/${packageName}.lock" -readonly hostsFilter='/localhost/d;/^#/d;/^[^0-9]/d;s/^0\.0\.0\.0.//;s/^127\.0\.0\.1.//;s/[[:space:]]*#.*$//;s/[[:cntrl:]]$//;s/[[:space:]]//g;/[`~!@#\$%\^&\*()=+;:"'\'',<>?/\|[{}]/d;/]/d;/\./!d;/^$/d;/[^[:alnum:]_.-]/d;' -# Validating domains filter -readonly domainsFilter='/^#/d;s/[[:space:]]*#.*|[[:space:]]*$|[[:cntrl:]]$//g;/^[[:space:]]*$/d;/^-|^\.|\.\.|-$|\.$|^[0-9.]+$|^[^[:alnum:]]|[`~!@#\$%\^&\*()=+;:"'"'"',<>?/\|{}]/d;/\./!d' -# Lax domains filter -#readonly domainsFilter='/^#/d;s/[[:space:]]*#.*|[[:space:]]*$|[[:cntrl:]]$//g;/^[[:space:]]*$/d;/^[^[:alnum:]._-]|[`~!@#\$%\^&\*()=+;:"'"'"',<>?/\|{}]/d;/\./!d' -readonly adBlockPlusFilter='/^#/d;/^!/d;s/[[:space:]]*#.*$//;s/^||//;s/\^$//;s/[[:space:]]*$//;s/[[:cntrl:]]$//;/[[:space:]]/d;/[`~!@#\$%\^&\*()=+;:"'\'',<>?/\|[{}]/d;/]/d;/\./!d;/^$/d;/[^[:alnum:]_.-]/d;' -readonly dnsmasqFileFilter='\|^server=/[[:alnum:]_.-].*/|!d;s|server=/||;s|/.*$||' -readonly dnsmasq2FileFilter='\|^local=/[[:alnum:]_.-].*/|!d;s|local=/||;s|/.*$||' -readonly dnsmasq3FileFilter='\|^address=/[[:alnum:]_.-].*/|!d;s|address=/||;s|/.*$||' -readonly _DOT_='.' -readonly __DOT__='[w]' -readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m' -readonly __OK__='\033[0;32m[\xe2\x9c\x93]\033[0m' -readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m' -readonly __FAIL__='\033[0;31m[\xe2\x9c\x97]\033[0m' -readonly _WARN_='\033[0;33m\xe2\x9c\x94\033[0m' -readonly __WARN__='\033[0;33m[\xe2\x9c\x94]\033[0m' -readonly _ERROR_='\033[0;31m[ERROR]\033[0m' -readonly _WARNING_='\033[0;33m[WARN]\033[0m' -# shellcheck disable=SC2155 -readonly ipset="$(command -v ipset)" -# shellcheck disable=SC2155 -readonly nft="$(command -v nft)" -readonly canaryDomainsMozilla='use-application-dns.net' -readonly canaryDomainsiCloud='mask.icloud.com mask-h2.icloud.com' -readonly triggersReload='parallel_downloads debug download_timeout allowed_domain blocked_domain allowed_url blocked_url dns config_update_enabled config_update_url dnsmasq_config_file_url curl_additional_param curl_max_file_size curl_retry' -readonly triggersRestart='compressed_cache compressed_cache_dir force_dns led force_dns_port' - -# Silence "Command failed: Not found" for redundant procd service delete calls -__UBUS_BIN="$(command -v ubus || echo /bin/ubus)" -ubus() { - if [ "$1" = "call" ] && [ "$2" = "service" ] && [ "$3" = "delete" ]; then - "$__UBUS_BIN" "$@" >/dev/null 2>&1 || true - else - "$__UBUS_BIN" "$@" - fi -} - -dl_command= -dl_flag= -isSSLSupported= -loadEnvironmentFlag= -loadPackageConfigFlag= -outputAllowFilter= -outputBlockedCountFilter= -outputFilter= -outputFilterIPv6= -outputFile= -outputGzip= -outputCache= -stripToDomainsFilter= -triggerStatus= -awk='awk' -allowed_url= -blocked_url= -fw4_restart_flag= -adbf_boot_flag= -dnsmasq_features= -dnsmasq_ubus= - -# package config variables -allow_non_ascii= -canary_domains_icloud= -canary_domains_mozilla= -compressed_cache= -config_update_enabled= -debug_init_script= -debug_performance= -dnsmasq_sanity_check= -dnsmasq_validity_check= -enabled= -force_dns= -ipv6_enabled= -parallel_downloads= -procd_trigger_wan6= -sanity_check= -update_config_sizes= -allowed_domain= -blocked_domain= -compressed_cache_dir= -config_update_url= -curl_additional_param= -curl_max_file_size= -curl_retry= -dns= -dnsmasq_config_file_url= -dnsmasq_instance= -download_timeout= -force_dns_interface= -force_dns_port= -heartbeat_domain= -heartbeat_sleep_timeout= -led= -pause_timeout= -procd_boot_wan_timeout= -smartdns_instance= -verbosity= +readonly _ucode="ucode -S -L /lib/${packageName} /lib/${packageName}/cli.uc --" +_procd_svc_data= +_fw4_restart= # shellcheck disable=SC1091 . "${IPKG_INSTROOT}/lib/functions.sh" # shellcheck disable=SC1091 . "${IPKG_INSTROOT}/lib/functions/network.sh" -# shellcheck disable=SC1091 -. "${IPKG_INSTROOT}/usr/share/libubox/jshn.sh" -append_newline() { is_newline_ending "$1" || echo '' >> "$1"; } -check_ipset() { { command -v ipset && /usr/sbin/ipset help hash:net; } >/dev/null 2>&1; } -check_nft() { command -v nft >/dev/null 2>&1; } -check_dnsmasq() { command -v dnsmasq >/dev/null 2>&1; } -check_dnsmasq_feature () { - [ -z "$dnsmasq_features" ] && dnsmasq_features="$(dnsmasq --version | grep -m1 'Compile time options:' | cut -d: -f2) " - case "$1" in - idn) [ "${dnsmasq_features#* IDN }" != "$dnsmasq_features" ];; - ipset) [ "${dnsmasq_features#* ipset }" != "$dnsmasq_features" ];; - nftset) [ "${dnsmasq_features#* nftset }" != "$dnsmasq_features" ];; - esac -} -check_dnsmasq_ipset() { check_ipset && check_dnsmasq_feature 'ipset'; } -check_dnsmasq_nftset() { check_nft && check_dnsmasq_feature 'nftset'; } -check_smartdns() { command -v smartdns >/dev/null 2>&1; } -check_smartdns_ipset() { check_smartdns && check_ipset; } -check_smartdns_nftset() { check_smartdns && check_nft; } -check_unbound() { command -v unbound >/dev/null 2>&1; } -append_url() { - local cfg="$1" allow_var="${2:-allowed_url}" block_var="${3:-blocked_url}" - local old_value - local en action url - config_get_bool en "$cfg" enabled '1' - config_get action "$cfg" action 'block' - config_get url "$cfg" url - if [ "$en" = '1' ]; then - if [ "$action" = 'allow' ]; then - old_value=$(eval echo "\$$allow_var") - old_value="${old_value:+$old_value }${url}" - eval "$allow_var"="\$old_value" - else - old_value=$(eval echo "\$$block_var") - old_value="${old_value:+$old_value }${url}" - eval "$block_var"="\$old_value" - fi - fi -} -adb_config_cache() { - local param="$1" var="$2" - local _reload="$triggersReload" - local _restart="$triggersRestart" - local i ret - case "$param" in - create|set) - cp -f "$uciConfigFile" "$runningConfigFile" - ;; - get) - case "$var" in - trigger_fw4) - if [ -s "$runningConfigFile" ]; then - local UCI_CONFIG_DIR="${runningConfigFile%/*}" - is_fw4_restart_needed && ret='true' - fi - printf "%b" "$ret" - return - ;; - trigger_service) - local old_allowed_url old_blocked_url - if [ ! -s "$runningConfigFile" ]; then - ret='on_boot' - elif ! cmp -s "$uciConfigFile" "$runningConfigFile"; then -# ret='restart' -# else - local current_allowed_url current_blocked_url - config_load "$uciConfigFile" - config_foreach append_url 'file_url' current_allowed_url current_blocked_url - if [ -z "$allowed_url" ] || [ -z "$blocked_url" ]; then - config_load "$runningConfigFile" - config_foreach append_url 'file_url' allowed_url blocked_url - fi - for i in $_reload; do - local val_current val_old UCI_CONFIG_DIR - case "$i" in - allowed_url) - val_current="$current_allowed_url" - val_old="$allowed_url" - ;; - blocked_url) - val_current="$current_blocked_url" - val_old="$blocked_url" - ;; - *) - UCI_CONFIG_DIR= - val_current="$(uci_get "$packageName" 'config' "$i")" - UCI_CONFIG_DIR="${runningConfigFile%/*}" - val_old="$(uci_get "$packageName" 'config' "$i")" - ;; - esac - if [ "$val_current" != "$val_old" ]; then - ret='download' - unset _restart - break - fi - done - for i in $_restart; do - local val_current val_old UCI_CONFIG_DIR - UCI_CONFIG_DIR= - val_current="$(uci_get "$packageName" 'config' "$i")" - UCI_CONFIG_DIR="${runningConfigFile%/*}" - val_old="$(uci_get "$packageName" 'config' "$i")" - if [ "$val_current" != "$val_old" ]; then - ret='restart' - break - fi - done - fi - printf "%b" "$ret" - return - ;; - *) - local UCI_CONFIG_DIR="${runningConfigFile%/*}" - ret="$(uci_get "$packageName" 'config' "$var")" - printf "%b" "$ret" - return - ;; - esac - ;; - esac -} -count_blocked_domains() { - if [ -n "$outputBlockedCountFilter" ]; then - [ -f "$outputFile" ] && sed "$outputBlockedCountFilter" "$outputFile" | wc -l || echo '0' - else - [ -f "$outputFile" ] && wc -l < "$outputFile" || echo '0' - fi -} -debug() { local __i __j; for __i in "$@"; do eval "__j=\$$__i"; echo "${__i}: ${__j} "; done; } -debug_log() { local __i __j; for __i in "$@"; do eval "__j=\$$__i"; logger -t "$packageName" "${__i}: ${__j} "; done; } -dns_set_output_values() { - case "$1" in - dnsmasq.addnhosts) - outputFilter="$dnsmasqAddnhostsOutputFormatFilter" - outputFile="$dnsmasqAddnhostsFile" - outputCache="$dnsmasqAddnhostsCache" - outputGzip="${compressed_cache_dir}/${dnsmasqAddnhostsGzip}" - outputParseFilter="$dnsmasqAddnhostsOutputParseFilter" - if [ -n "$ipv6_enabled" ]; then - outputFilterIPv6="$dnsmasqAddnhostsOutputFormatFilterIPv6" - fi - ;; - dnsmasq.conf) - outputFilter="$dnsmasqConfOutputFormatFilter" - outputFile="$dnsmasqConfFile" - outputCache="$dnsmasqConfCache" - outputGzip="${compressed_cache_dir}/${dnsmasqConfGzip}" - outputParseFilter="$dnsmasqConfOutputParseFilter" - ;; - dnsmasq.ipset) - outputFilter="$dnsmasqIpsetOutputFormatFilter" - outputFile="$dnsmasqIpsetFile" - outputCache="$dnsmasqIpsetCache" - outputGzip="${compressed_cache_dir}/${dnsmasqIpsetGzip}" - outputParseFilter="$dnsmasqIpsetOutputParseFilter" - ;; - dnsmasq.nftset) - if [ -n "$ipv6_enabled" ]; then - outputFilter="$dnsmasqNftsetOutputFormatFilterIPv6" - else - outputFilter="$dnsmasqNftsetOutputFormatFilter" - fi - outputFile="$dnsmasqNftsetFile" - outputCache="$dnsmasqNftsetCache" - outputGzip="${compressed_cache_dir}/${dnsmasqNftsetGzip}" - outputParseFilter="$dnsmasqNftsetOutputParseFilter" - ;; - dnsmasq.servers) - outputFilter="$dnsmasqServersOutputFormatFilter" - outputFile="$dnsmasqServersFile" - outputCache="$dnsmasqServersCache" - outputGzip="${compressed_cache_dir}/${dnsmasqServersGzip}" - outputParseFilter="$dnsmasqServersOutputParseFilter" - outputAllowFilter="$dnsmasqServersAllowFilter" - outputBlockedCountFilter="$dnsmasqServersBlockedCountFilter" - ;; - smartdns.domainset) - outputFilter="$smartdnsDomainSetOutputFormatFilter" - outputFile="$smartdnsDomainSetFile" - outputCache="$smartdnsDomainSetCache" - outputGzip="${compressed_cache_dir}/${smartdnsDomainSetGzip}" - outputConfig="$smartdnsDomainSetConfig" - outputParseFilter="$smartdnsDomainSetOutputParseFilter" - ;; - smartdns.ipset) - outputFilter="$smartdnsIpsetOutputFormatFilter" - outputFile="$smartdnsIpsetFile" - outputCache="$smartdnsIpsetCache" - outputGzip="${compressed_cache_dir}/${smartdnsIpsetGzip}" - outputConfig="$smartdnsIpsetConfig" - outputParseFilter="$smartdnsIpsetOutputParseFilter" - ;; - smartdns.nftset) - outputFilter="$smartdnsNftsetOutputFormatFilter" - outputFile="$smartdnsNftsetFile" - outputCache="$smartdnsNftsetCache" - outputGzip="${compressed_cache_dir}/${smartdnsNftsetGzip}" - outputConfig="$smartdnsNftsetConfig" - outputParseFilter="$smartdnsNftsetOutputParseFilter" - ;; - unbound.adb_list) - outputFilter="$unboundOutputFormatFilter" - outputFile="$unboundFile" - outputCache="$unboundCache" - outputGzip="${compressed_cache_dir}/${unboundGzip}" - outputParseFilter="$unboundOutputParseFilter" - ;; - esac - resolver 'on_load' -} -dnsmasq_hup() { killall -q -s HUP dnsmasq; } -dnsmasq_kill() { killall -q -s KILL dnsmasq; } -dnsmasq_restart() { /etc/init.d/dnsmasq restart >/dev/null 2>&1; } -is_enabled() { uci_get "$1" 'config' 'enabled' '0'; } -is_fw4_restart_needed() { - [ -n "$fw4_restart_flag" ] && return 0 - local dns force_dns - dns="$(uci_get "$packageName" 'config' 'dns' 'dnsmasq.servers')" - force_dns="$(uci_get "$packageName" 'config' 'force_dns' '1')" - if [ "$force_dns" = '1' ]; then - return 0 - elif [ "$dns" = 'dnsmasq.ipset' ]; then - return 0 - elif [ "$dns" = 'dnsmasq.nftset' ]; then - return 0 - elif [ "$dns" = 'smartdns.ipset' ]; then - return 0 - elif [ "$dns" = 'smartdns.nftset' ]; then - return 0 - else - return 1 - fi -} -is_integer() { case "$1" in ''|*[!0-9]*) return 1;; esac; [ "$1" -ge 1 ] && [ "$1" -le 65535 ] || return 1; return 0; } -is_greater() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } -is_greater_or_equal() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" = "$2"; } -# shellcheck disable=SC3057 -is_https_url() { [ "${1:0:8}" = "https://" ]; } -is_newline_ending() { [ "$(tail -c1 "$1" | wc -l)" -ne '0' ]; } -is_port_listening() { - local hex - is_integer "$1" || return 1 - hex="$(printf '%04X' "$1")" - # TCP: state 0A == LISTEN - if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h && $4=="0A") {found=1}} END{exit found?0:1}' /proc/net/tcp /proc/net/tcp6 2>/dev/null; then - return 0 - fi - # UDP: presence indicates a bound socket - if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h) {found=1}} END{exit found?0:1}' /proc/net/udp /proc/net/udp6 2>/dev/null; then - return 0 - fi - return 1 -} -is_present() { command -v "$1" >/dev/null 2>&1; } -is_running() { - local i j - i="$(json get status)" - j="$(ubus_get_data status)" - if [ "$i" = 'statusStopped' ] || [ -z "${i}${j}" ]; then - return 1 - else - return 0 - fi -} -ipset() { "$ipset" "$@" >/dev/null 2>&1; } -get_mem_available() { - local ram swap - ram="$( ubus call system info | jsonfilter -e '@.memory.available' )" - swap="$( ubus call system info | jsonfilter -e '@.swap.free' )" - echo "$((ram + swap))"; -} -get_mem_total() { - local ram swap - ram="$( ubus call system info | jsonfilter -e '@.memory.total' )" - swap="$( ubus call system info | jsonfilter -e '@.swap.total' )" - echo "$((ram + swap))"; -} -led_on(){ if [ -n "${1}" ] && [ -e "${1}/trigger" ]; then echo 'default-on' > "${1}/trigger" 2>&1; fi; } -led_off(){ if [ -n "${1}" ] && [ -e "${1}/trigger" ]; then echo 'none' > "${1}/trigger" 2>&1; fi; } -logger() { /usr/bin/logger -t "$packageName" "$@"; } -logger_debug() { [ -n "$debug_performance" ] && /usr/bin/logger -t "$packageName [$$]" "$@"; } -nft() { "$nft" "$@" >/dev/null 2>&1; } -output_dot() { output 1 "$_DOT_"; output 2 "$__DOT__"; } -output_ok() { output 1 "$_OK_"; output 2 "$__OK__\n"; } -output_okn() { output 1 "$_OK_\n"; output 2 "$__OK__\n"; } -output_warn() { output 1 "$_WARN_"; output 2 "$__WARN__\n"; } -output_warnn() { output 1 "$_WARN_\n"; output 2 "$__WARN__\n"; } -output_fail() { output 1 "$_FAIL_"; output 2 "$__FAIL__\n"; } -output_failn() { output 1 "$_FAIL_\n"; output 2 "$__FAIL__\n"; } -output_dns() { - case "$dns" in - dnsmasq.*) output 2 "[DNSM] $*";; - smartdns.*) output 2 "[SMRT] $*";; - unbound.*) output 2 "[UNBD] $*";; - esac -} -output_error() { output "${_ERROR_} $*!\n"; } -output_warning() { output "${_WARNING_} $*!\n"; } -print_json_bool() { json_init; json_add_boolean "$1" "$2"; json_dump; json_cleanup; } -print_json_int() { json_init; json_add_int "$1" "$2"; json_dump; json_cleanup; } -print_json_string() { json_init; json_add_string "$1" "$2"; json_dump; json_cleanup; } -sanitize_domain() { printf '%s' "$1" | sed -E 's#^[a-z]+://##; s#/.*$##; s/:.*$//'; } -sanitize_dir() { [ -d "$(readlink -fn "$1")" ] && readlink -fn "$1"; } -smartdns_restart() { /etc/init.d/smartdns restart >/dev/null 2>&1; } -# shellcheck disable=SC3060 -str_contains() { [ "${1//$2}" != "$1" ]; } -str_contains_word() { echo "$1" | grep -qw "$2"; } -str_first_word() { echo "${1%% *}"; } -# shellcheck disable=SC2018,SC2019 -str_to_lower() { echo "$1" | tr 'A-Z' 'a-z'; } -# shellcheck disable=SC2018,SC2019 -str_to_upper() { echo "$1" | tr 'a-z' 'A-Z'; } -# shellcheck disable=SC3060 -str_replace() { echo "${1//$2/$3}"; } -ubus_get_data() { ubus call service list "{\"name\":\"$packageName\"}" | jsonfilter -e "@['${packageName}'].instances.main.data.${1}"; } -ubus_get_ports() { ubus call service list "{\"name\":\"$packageName\"}" | jsonfilter -e "@['${packageName}'].instances.main.data.firewall.*.dest_port"; } -uci_get_protocol() { uci_get 'network' "$1" 'proto'; } -unbound_restart() { /etc/init.d/unbound restart >/dev/null 2>&1; } - -json() { - { - flock -x 209 - local status message stats i - local action="$1" param="$2" value="$3"; shift 3; local info="$*"; - local _current_namespace="$_JSON_PREFIX" - json_set_namespace "${packageName//-/_}_" - [ "$param" = 'error' ] && param='errors' - [ "$param" = 'warning' ] && param='warnings' - { json_load_file "$runningStatusFile" || json_init; } >/dev/null 2>&1 - { json_select 'data' || { json_add_object 'data'; json_close_object; json_select 'data'; }; } >/dev/null 2>&1 - case "${action}:${param}" in - 'get:errors'|'get:warnings') - json_select "$param" >/dev/null 2>&1 || return - if [ -z "$value" ]; then - json_get_keys i - else - json_select "$value" >/dev/null 2>&1 - case "${info:-code}" in - 'code'|'info') json_get_var 'i' "$info" >/dev/null 2>&1;; - esac - fi - printf "%b" "$i" - json_set_namespace "$_current_namespace" - return - ;; - get:*) - json_get_var 'i' "$param" >/dev/null 2>&1 - printf "%b" "$i" - json_set_namespace "$_current_namespace" - return - ;; - 'add:errors'|'add:warnings') - { json_select "$param" || json_add_array "$param"; } >/dev/null 2>&1 - json_add_object "" - json_add_string 'code' "$value" - json_add_string 'info' "$info" - json_close_object - json_select .. - ;; - add:*) - json_add_string "$param" "$value" - ;; - 'del:all') - json_add_string status '' - json_add_string message '' - json_add_string stats '' - json_add_array errors - json_close_array - json_add_array warnings - json_close_array - ;; - 'del:errors'|'del:warnings') - json_add_array "$param" - json_close_array - ;; - del:*) - json_add_string "$param" '' - ;; - 'set:status'|'set:message'|'set:stats') - json_add_string "$param" "$value" - ;; - esac - json_add_string 'version' "$PKG_VERSION" - json_add_string 'packageCompat' "$packageCompat" - json_select .. - mkdir -p "${runningStatusFile%/*}" - json_dump > "$runningStatusFile" - sync - json_set_namespace "$_current_namespace" - } 209>"$runningStatusFileLock" -} - -get_local_filesize() { - local file="$1" size - [ -f "$file" ] || return 0 - if is_present stat; then - size="$(stat -c%s "$file")" - elif is_present wc; then - size="$(wc -c < "$file")" - fi -# shellcheck disable=SC3037 - echo -en "$size" -} - -get_url_filesize() { - local url="$1" size size_command timeout_sec=2 - [ -n "$url" ] || return 0 - if is_present 'curl'; then - # shellcheck disable=SC1017 - size_command='curl --silent --insecure --fail --head --request GET' - size="$($size_command --connect-timeout $timeout_sec "$url" | awk -F": " '{IGNORECASE=1}/content-length/ {gsub(/\r/, ""); print $2}' )" - fi - - # Check if size is empty and fallback to uclient-fetch if necessary - if [ -z "$size" ] && is_present 'uclient-fetch' ; then - # shellcheck disable=SC1017 - size_command='uclient-fetch --spider' - size="$($size_command --timeout $timeout_sec "$url" -O /dev/null 2>&1 | sed -n '/^Download/ s/.*(\([0-9]*\) bytes).*/\1/p')" - fi - # shellcheck disable=SC3037 - echo -en "$size" -} - -# shellcheck disable=SC3060 -output() { - [ -z "$verbosity" ] && verbosity="$(uci_get "$packageName" 'config' 'verbosity' '1')" - [ "$#" -ne '1' ] && { - case "$1" in [0-9]) [ $((verbosity & $1)) -gt 0 ] && shift || return 0;; esac } - local msg="$*" queue="/dev/shm/$packageName-output" - [ -t 1 ] && printf "%b" "$msg" - [ "$msg" != "${msg//\\n}" ] && { - [ -s "$queue" ] && msg="$(cat "$queue")${msg}" && rm -f "$queue" - msg="$(printf "%b" "$msg" | sed 's/\x1b\[[0-9;]*m//g')" - logger -t "$packageName [$$]" "$(printf "%b" "$msg")" - } || printf "%b" "$msg" >> "$queue" -} - -uci_add_list_if_new() { - local PACKAGE="$1" - local CONFIG="$2" - local OPTION="$3" - local VALUE="$4" - local i - [ -n "$PACKAGE" ] && [ -n "$CONFIG" ] && [ -n "$OPTION" ] && [ -n "$VALUE" ] || return 1 - for i in $(uci_get "$PACKAGE" "$CONFIG" "$OPTION"); do - [ "$i" = "$VALUE" ] && return 0 - done - uci_add_list "$PACKAGE" "$CONFIG" "$OPTION" "$VALUE" -} - -uci_changes() { - local PACKAGE="$1" - local CONFIG="$2" - local OPTION="$3" - [ -s "${UCI_CONFIG_DIR:-/etc/config/}${PACKAGE}" ] && \ - [ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} changes "$PACKAGE${CONFIG:+.$CONFIG}${OPTION:+.$OPTION}")" ] -} - -get_text() { - local r="$1"; shift; - case "$r" in - errorConfigValidationFail) printf "The %s config validation failed" "$packageName";; - errorServiceDisabled) printf "The %s is currently disabled" "$packageName";; - errorNoDnsmasqIpset) - printf "The dnsmasq ipset support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support ipset" "$packageName";; - errorNoIpset) - printf "The dnsmasq ipset support is enabled in %s, but ipset is either not installed or installed ipset does not support 'hash:net' type" "$packageName";; - errorNoDnsmasqNftset) - printf "The dnsmasq nft set support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support nft set" "$packageName";; - errorNoNft) printf "The dnsmasq nft sets support is enabled in %s, but nft is not installed" "$packageName";; - errorNoWanGateway) printf "The %s failed to discover WAN gateway" "$serviceName";; - errorOutputDirCreate) printf "Failed to create directory for %s file" "$@";; - errorOutputFileCreate) printf "Failed to create %s file" "$@";; - errorFailDNSReload) printf "Failed to restart/reload DNS resolver";; - errorSharedMemory) printf "Failed to access shared memory";; - errorSorting) printf "Failed to sort data file";; - errorOptimization) printf "Failed to optimize data file";; - errorAllowListProcessing) printf "Failed to process allow-list";; - errorDataFileFormatting) printf "Failed to format data file";; - errorCopyingDataFile) printf "Failed to copy data file to '%s'" "$@";; - errorMovingDataFile) printf "Failed to move data file to '%s'" "$@";; - errorCreatingCompressedCache) printf "Failed to create compressed cache";; - errorRemovingTempFiles) printf "Failed to remove temporary files";; - errorRestoreCompressedCache) printf "Failed to unpack compressed cache";; - errorRestoreCache) printf "Failed to move '%s' to '%s'" "$outputCache" "$outputFile";; - errorOhSnap) printf "Failed to create block-list or restart DNS resolver";; - errorStopping) printf "Failed to stop %s" "$serviceName";; - errorDNSReload) printf "Failed to reload/restart DNS resolver";; - errorDownloadingConfigUpdate) printf "Failed to download Config Update file";; - errorDownloadingList) printf "Failed to download %s" "$@";; - errorParsingConfigUpdate) printf "Failed to parse Config Update file";; - errorParsingList) printf "Failed to parse";; - errorNoSSLSupport) printf "No HTTPS/SSL support on device";; - errorCreatingDirectory) printf "Failed to create output/cache/gzip file directory";; - errorDetectingFileType) printf "Failed to detect format";; - errorNothingToDo) printf "No blocked list URLs nor blocked-domains enabled";; - errorTooLittleRam) printf "Free ram (%s) is not enough to process all enabled block-lists" "$@";; - errorCreatingBackupFile) printf "Failed to create backup file %s" "$@";; - errorDeletingDataFile) printf "Failed to delete data file %s" "$@";; - errorRestoringBackupFile) printf "Failed to restore backup file %s" "$@";; - errorNoOutputFile) printf "Failed to create final block-list %s" "$@";; - errorNoHeartbeat) printf "Heartbeat domain is not accessible after resolver restart";; - - statusNoInstall) printf "The %s is not installed or not found" "$serviceName";; - statusStopped) printf "stopped";; - statusStarting) printf "starting";; - statusRestarting) printf "restarting";; - statusForceReloading) printf "force-reloading";; - statusDownloading) printf "downloading";; - statusProcessing) printf "processing";; - statusFail) printf "failed to start";; - statusSuccess) printf "success";; - statusTriggerBootWait) printf "waiting for trigger (on_boot)";; - statusTriggerStartWait) printf "waiting for trigger (on_start)";; - - warningExternalDnsmasqConfig) - printf "Use of external dnsmasq config file detected, please set 'dns' option to 'dnsmasq.conf'";; - warningMissingRecommendedPackages) printf "Some recommended packages are missing";; - warningInvalidCompressedCacheDir) printf "Invalid compressed cache directory '%s'" "$@";; - warningFreeRamCheckFail) printf "Can't detect free RAM";; - warningSanityCheckTLD) printf "Sanity check discovered TLDs in %s" "$@";; - warningSanityCheckLeadingDot) printf "Sanity check discovered leading dots in %s" "$@";; - warningInvalidDomainsRemoved) printf "Removed %s invalid domain entries from block-list (domains starting with -/./numbers or containing invalid patterns)" "$@";; - - *) printf "Unknown error/warning '%s'" "$@";; - esac -} - -load_network() { - local param="$1" - local i j wan_if wan_gw - local counter wan_if_timeout="$procd_boot_wan_timeout" wan_gw_timeout='5' - counter=0 - while [ -z "$wan_if" ]; do - network_flush_cache - network_find_wan wan_if - if [ -n "$wan_if" ]; then - output 1 "WAN interface found: '${wan_if}'.\n" - output 2 "[BOOT] WAN interface found: '${wan_if}'.\n" - break - fi - if [ "$counter" -gt "$wan_if_timeout" ]; then - output 1 "WAN interface timeout, assuming 'wan'.\n" - output 2 "[BOOT] WAN interface timeout, assuming 'wan'.\n" - wan_if='wan' - break - fi - counter=$((counter+1)) - output 1 "Waiting to discover WAN Interface...\n" - output 2 "[BOOT] Waiting to discover WAN Interface...\n" - sleep 1 - done - - counter=0 - if [ "$(uci_get_protocol "$wan_if")" = 'pppoe' ]; then - wan_gw_timeout=$((wan_gw_timeout+10)) - fi - while [ "$counter" -le "$wan_gw_timeout" ]; do - network_flush_cache - network_get_gateway wan_gw "$wan_if" - if [ -n "$wan_gw" ]; then - output 1 "WAN gateway found: '${wan_gw}.'\n" - output 2 "[BOOT] WAN gateway found: '${wan_gw}.'\n" - return 0 - fi - counter=$((counter+1)) - output 1 "Waiting to discover $wan_if gateway...\n" - output 2 "[BOOT] Waiting to discover $wan_if gateway...\n" - sleep 1 - done - json add error 'errorNoWanGateway' - output_error "$(get_text 'errorNoWanGateway')" - return 1 -} - -detect_file_type() { - local file="$1" - if [ "$(head -1 "$file")" = '[Adblock Plus]' ] || \ - grep -q '^||' "$file"; then - echo 'adblockplus' - elif grep -q '^server=' "$file"; then - echo 'dnsmasq' - elif grep -q '^local=' "$file"; then - echo 'dnsmasq2' - elif grep -q '^address=' "$file"; then - echo 'dnsmasq3' - elif grep -q -e '^0\.0\.0\.0\s' -e '^127\.0\.0\.1\s' "$file"; then - echo 'hosts' - elif [ -n "$(sed "$domainsFilter" "$file" | head -1)" ]; then - echo 'domains' - fi -} - -load_package_config() { - config_load "$packageName" - config_get_bool allow_non_ascii 'config' 'allow_non_ascii' '0' - config_get_bool canary_domains_icloud 'config' 'canary_domains_icloud' '0' - config_get_bool canary_domains_mozilla 'config' 'canary_domains_mozilla' '0' - config_get_bool compressed_cache 'config' 'compressed_cache' '0' - config_get_bool config_update_enabled 'config' 'config_update_enabled' '0' - config_get_bool debug_init_script 'config' 'debug_init_script' '0' - config_get_bool debug_performance 'config' 'debug_performance' '0' - config_get_bool dnsmasq_sanity_check 'config' 'dnsmasq_sanity_check' '1' - config_get_bool dnsmasq_validity_check 'config' 'dnsmasq_validity_check' '0' - config_get_bool enabled 'config' 'enabled' '0' - config_get_bool force_dns 'config' 'force_dns' '1' - config_get_bool ipv6_enabled 'config' 'ipv6_enabled' '0' - config_get_bool parallel_downloads 'config' 'parallel_downloads' '1' - config_get_bool procd_trigger_wan6 'config' 'procd_trigger_wan6' '0' - config_get_bool update_config_sizes 'config' 'update_config_sizes' '1' - config_get allowed_domain 'config' 'allowed_domain' - config_get blocked_domain 'config' 'blocked_domain' - config_get compressed_cache_dir 'config' 'compressed_cache_dir' '/etc' - config_get config_update_url 'config' 'config_update_url' 'https://cdn.jsdelivr.net/gh/openwrt/packages/net/adblock-fast/files/adblock-fast.config.update' - config_get curl_additional_param 'config' 'curl_additional_param' - config_get curl_max_file_size 'config' 'curl_max_file_size' - config_get curl_retry 'config' 'curl_retry' '3' - config_get dns 'config' 'dns' 'dnsmasq.servers' - config_get dnsmasq_config_file_url 'config' 'dnsmasq_config_file_url' - config_get dnsmasq_instance 'config' 'dnsmasq_instance' '*' - config_get download_timeout 'config' 'download_timeout' '20' - config_get force_dns_interface 'config' 'force_dns_interface' 'lan' - config_get force_dns_port 'config' 'force_dns_port' '53 853' - config_get heartbeat_domain 'config' 'heartbeat_domain' 'heartbeat.melmac.ca' - config_get heartbeat_sleep_timeout 'config' 'heartbeat_sleep_timeout' '10' - config_get led 'config' 'led' - config_get pause_timeout 'config' 'pause_timeout' '20' - config_get procd_boot_wan_timeout 'config' 'procd_boot_wan_timeout' '60' - config_get smartdns_instance 'config' 'smartdns_instance' '*' - config_get verbosity 'config' 'verbosity' '2' - - [ "$allow_non_ascii" = '1' ] || unset allow_non_ascii - [ "$canary_domains_icloud" = '1' ] || unset canary_domains_icloud - [ "$canary_domains_mozilla" = '1' ] || unset canary_domains_mozilla - [ "$compressed_cache" = '1' ] || unset compressed_cache - [ "$config_update_enabled" = '1' ] || unset config_update_enabled - [ "$debug_init_script" = '1' ] || unset debug_init_script - [ "$debug_performance" = '1' ] || unset debug_performance - [ "$dnsmasq_sanity_check" = '1' ] || unset dnsmasq_sanity_check - [ "$dnsmasq_validity_check" = '1' ] || unset dnsmasq_validity_check - [ "$enabled" = '1' ] || unset enabled - [ "$force_dns" = '1' ] || unset force_dns - [ "$ipv6_enabled" = '1' ] || unset ipv6_enabled - [ "$parallel_downloads" = '1' ] || unset parallel_downloads - [ "$procd_trigger_wan6" = '1' ] || unset procd_trigger_wan6 - [ "$update_config_sizes" = '1' ] || unset update_config_sizes - - dns_set_output_values "$dns" - [ "$heartbeat_domain" = '-' ] && unset heartbeat_domain || heartbeat_domain="$(sanitize_domain "$heartbeat_domain")" - if [ "$(sanitize_dir "$compressed_cache_dir")" = '/' ]; then - compressed_cache_dir='' - elif [ -n "$(sanitize_dir "$compressed_cache_dir")" ]; then - compressed_cache_dir="$(sanitize_dir "$compressed_cache_dir")" - else - compressed_cache_dir="/etc" - fi - - unset loadEnvironmentFlag - loadPackageConfigFlag='true' -} - -load_dl_command() { - # Prefer curl because it supports the file:// scheme. - if is_present 'curl'; then - dl_command='curl -f --silent --insecure' - dl_command="${dl_command}${curl_additional_param:+ $curl_additional_param}" - dl_command="${dl_command}${curl_max_file_size:+ --max-filesize $curl_max_file_size}" - dl_command="${dl_command}${curl_retry:+ --retry $curl_retry}" - dl_command="${dl_command}${download_timeout:+ --connect-timeout $download_timeout}" - dl_flag='-o' - elif is_present '/usr/libexec/wget-ssl'; then - dl_command='/usr/libexec/wget-ssl --no-check-certificate -q' - dl_command="${dl_command}${download_timeout:+ --timeout $download_timeout}" - dl_flag="-O" - size_command='/usr/libexec/wget-ssl --no-check-certificate -q -O /dev/null --server-response' - size_command="${size_command}${download_timeout:+ --timeout $download_timeout}" - elif is_present wget && wget --version 2>/dev/null | grep -q "+https"; then - dl_command="wget --no-check-certificate -q" - dl_command="${dl_command}${download_timeout:+ --timeout $download_timeout}" - dl_flag="-O" - size_command='wget --no-check-certificate -q -O /dev/null --server-response' - size_command="${size_command}${download_timeout:+ --timeout $download_timeout}" - else - dl_command="uclient-fetch --no-check-certificate -q" - dl_command="${dl_command}${download_timeout:+ --timeout $download_timeout}" - dl_flag="-O" - fi - if curl --version 2>/dev/null | grep -q "Protocols: .*https.*" \ - || wget --version 2>/dev/null | grep -q "+ssl"; then - isSSLSupported='true' - else - unset isSSLSupported - fi -} - -load_environment() { - local i j - local param="$1" validation_result="$2" - - [ -z "$loadEnvironmentFlag" ] || return 0 - [ -n "$loadPackageConfigFlag" ] || load_package_config - - if [ -z "$enabled" ]; then - json add error 'errorServiceDisabled' - output_error "$(get_text 'errorServiceDisabled')" - output "Run the following commands before starting service again:\n" - output "uci set ${packageName}.config.enabled='1'; uci commit $packageName;\n" - return 1 - fi - - if [ -n "$validation_result" ] && [ "$validation_result" != '0' ]; then - output 1 "$_FAIL_\n" - json add error 'errorConfigValidationFail' - output_error "$(get_text 'errorConfigValidationFail')" - output "Please check if the '$packageConfigFile' contains correct values for config options.\n" - return 1 - fi - - if [ -n "$debug_init_script" ]; then - exec 1>>"/tmp/$packageName.log" - exec 2>&1 - set -x - fi - - # Check for resolver presence and error out on start - case "$dns" in - dnsmasq.*) - if ! check_dnsmasq; then - [ "$param" != 'quiet' ] && { json add error 'errorDNSReload'; output_error "Resolver 'dnsmasq' not found"; } - return 1 - fi - if check_dnsmasq_feature 'idn'; then - allow_non_ascii='' - fi - ;; - smartdns.*) - if ! check_smartdns; then - [ "$param" != 'quiet' ] && { json add error 'errorDNSReload'; output_error "Resolver 'smartdns' not found"; } - return 1 - fi - allow_non_ascii='' - ;; - unbound.*) - if ! check_unbound; then - [ "$param" != 'quiet' ] && { json add error 'errorDNSReload'; output_error "Resolver 'unbound' not found"; } - return 1 - fi - allow_non_ascii='true' - ;; - esac - - case "$dns" in - dnsmasq.ipset) - if ! check_dnsmasq_feature 'ipset'; then - if [ "$param" != 'quiet' ]; then - json add error 'errorNoDnsmasqIpset' -# output_error "$(get_text 'errorNoDnsmasqIpset')" - fi - dns='dnsmasq.servers' - fi - if ! ipset help hash:net; then - if [ "$param" != 'quiet' ]; then - json add error 'errorNoIpset' -# output_error "$(get_text 'errorNoIpset')" - fi - dns='dnsmasq.servers' - fi - ;; - dnsmasq.nftset) - if ! check_dnsmasq_feature 'nftset'; then - if [ "$param" != 'quiet' ]; then - json add error 'errorNoDnsmasqNftset' -# output_error "$(get_text 'errorNoDnsmasqNftset')" - fi - dns='dnsmasq.servers' - fi - if [ -z "$nft" ]; then - if [ "$param" != 'quiet' ]; then - json add error 'errorNoNft' -# output_error "$(get_text 'errorNoNft')" - fi - dns='dnsmasq.servers' - fi - ;; - smartdns.ipset) - if ! ipset help hash:net; then - if [ "$param" != 'quiet' ]; then - json add error 'errorNoIpset' -# output_error "$(get_text 'errorNoIpset')" - fi - dns='smartdns.domainset' - fi - ;; - smartdns.nftset) - if [ -z "$nft" ]; then - if [ "$param" != 'quiet' ]; then - json add error 'errorNoNft' -# output_error "$(get_text 'errorNoNft')" - fi - dns='smartdns.domainset' - fi - ;; - esac - - if [ -n "$dnsmasq_config_file_url" ]; then - unset update_config_sizes - case "$dns" in - dnsmasq.conf) :;; - *) - dns='dnsmasq.conf' - if [ "$param" != 'quiet' ]; then - json add warning 'warningExternalDnsmasqConfig' - fi - ;; - esac - fi - - [ "$dns" = 'dnsmasq.addnhosts' ] || rm -f "$dnsmasqAddnhostsFile" "$dnsmasqAddnhostsCache" "${compressed_cache_dir}/${dnsmasqAddnhostsGzip}" - [ "$dns" = 'dnsmasq.conf' ] || rm -f "$dnsmasqConfCache" "${compressed_cache_dir}/${dnsmasqConfGzip}" - [ "$dns" = 'dnsmasq.ipset' ] || rm -f "$dnsmasqIpsetCache" "${compressed_cache_dir}/${dnsmasqIpsetGzip}" - [ "$dns" = 'dnsmasq.nftset' ] || rm -f "$dnsmasqNftsetCache" "${compressed_cache_dir}/${dnsmasqNftsetGzip}" - [ "$dns" = 'dnsmasq.servers' ] || rm -f "$dnsmasqServersFile" "$dnsmasqServersCache" "${compressed_cache_dir}/${dnsmasqServersGzip}" - [ "$dns" = 'smartdns.domainset' ] || rm -f "$smartdnsDomainSetFile" "$smartdnsDomainSetCache" "${compressed_cache_dir}/${smartdnsDomainSetGzip}" "$smartdnsDomainSetConfig" - [ "$dns" = 'smartdns.ipset' ] || rm -f "$smartdnsIpsetFile" "$smartdnsIpsetCache" "${compressed_cache_dir}/${smartdnsIpsetGzip}" "$smartdnsIpsetConfig" - [ "$dns" = 'smartdns.nftset' ] || rm -f "$smartdnsNftsetFile" "$smartdnsNftsetCache" "${compressed_cache_dir}/${smartdnsNftsetGzip}" "$smartdnsNftsetConfig" - [ "$dns" = 'unbound.adb_list' ] || rm -f "$unboundFile" "$unboundCache" "${compressed_cache_dir}/${unboundGzip}" - - for i in "$runningConfigFile" "$runningStatusFile" "$outputFile" "$outputCache" "$outputGzip" "$outputConfig"; do - [ -n "$i" ] || continue - if ! mkdir -p "${i%/*}"; then - if [ "$param" != 'quiet' ]; then - json add error 'errorOutputDirCreate' "$i" - fi - fi - done - - is_present 'gawk' && awk='gawk' - if ! is_present '/usr/libexec/grep-gnu' || ! is_present '/usr/libexec/sed-gnu' || \ - ! is_present '/usr/libexec/sort-coreutils' || ! is_present 'gawk'; then - local s - is_present 'gawk' || { json add warning 'warningMissingRecommendedPackages' 'gawk'; s="${s:+$s }gawk"; } - is_present '/usr/libexec/grep-gnu' || { json add warning 'warningMissingRecommendedPackages' 'grep'; s="${s:+$s }grep"; } - is_present '/usr/libexec/sed-gnu' || { json add warning 'warningMissingRecommendedPackages' 'sed'; s="${s:+$s }sed"; } - is_present '/usr/libexec/sort-coreutils' || { json add warning 'warningMissingRecommendedPackages' 'coreutils-sort'; s="${s:+$s }coreutils-sort"; } - if [ "$param" != 'quiet' ]; then - output_warning "$(get_text 'warningMissingRecommendedPackages'), install them by running:" - output "opkg update; opkg --force-overwrite install $s;" - fi - fi - - load_dl_command - - led="${led:+/sys/class/leds/$led}" - config_load "$packageName" - config_foreach append_url 'file_url' allowed_url blocked_url - loadEnvironmentFlag='true' - adb_file 'test_cache' && return 0 - adb_file 'test_gzip' && return 0 - if [ "$param" = 'on_boot' ]; then - load_network "$param" - return "$?" - else - return 0 - fi -} - -resolver() { - _dnsmasq_instance_get_confdir() { - local cfg_file - [ -z "$dnsmasq_ubus" ] && dnsmasq_ubus="$(ubus call service list '{"name":"dnsmasq"}')" - cfg_file="$(echo "$dnsmasq_ubus" | jsonfilter -e "@.dnsmasq.instances.${1}.command" \ - | awk '{gsub(/\\\//,"/");gsub(/[][",]/,"");for(i=1;i<=NF;i++)if($i=="-C"){print $(i+1);exit}}')" - awk -F= '/^conf-dir=/{print $2; exit}' "$cfg_file" - } - _dnsmasq_instance_config() { - local cfg="$1" param="$2" confdir - [ -s "/etc/config/dhcp" ] || return 0 - [ -n "$(uci_get dhcp "$cfg")" ] || return 1 - case "$param" in - dnsmasq.addnhosts) - # clean up other dnsmasq configs - confdir="$(_dnsmasq_instance_get_confdir "$cfg")" - [ -n "$confdir" ] && rm -f "${confdir}/${packageName}" - uci_remove_list 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile" - if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" = "$dnsmasqServersFile" ]; then - uci_remove 'dhcp' "$cfg" 'serversfile' - fi - # add dnsmasq addnhosts config - uci_add_list_if_new 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile" - ;; - cleanup|unbound.adb_list) - # clean up all dnsmasq configs - confdir="$(_dnsmasq_instance_get_confdir "$cfg")" - [ -n "$confdir" ] && rm -f "${confdir}/${packageName}" - uci_remove_list 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile" - uci_remove_list 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile" - if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" = "$dnsmasqServersFile" ]; then - uci_remove 'dhcp' "$cfg" 'serversfile' - fi - ;; - dnsmasq.conf|dnsmasq.ipset|dnsmasq.nftset) - # clean up other dnsmasq configs - uci_remove_list 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile" - if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" = "$dnsmasqServersFile" ]; then - uci_remove 'dhcp' "$cfg" 'serversfile' - fi - # add dnsmasq conf addnmount to point to adblock-fast file - uci_add_list_if_new 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile" - # add softlink to adblock-fast file - confdir="$(_dnsmasq_instance_get_confdir "$cfg")" - [ -n "$confdir" ] || return 1 - ln -sf "$dnsmasqConfFile" "${confdir}/${packageName}" - chmod 660 "${confdir}/${packageName}" - chown -h root:dnsmasq "${confdir}/${packageName}" >/dev/null 2>/dev/null - ;; - dnsmasq.servers) - # clean up other dnsmasq configs - uci_remove_list 'dhcp' "$cfg" 'addnhosts' "$dnsmasqAddnhostsFile" - confdir="$(_dnsmasq_instance_get_confdir "$cfg")" - [ -n "$confdir" ] && rm -f "${confdir}/${packageName}" - uci_remove_list 'dhcp' "$cfg" 'addnmount' "$dnsmasqConfFile" - # add dnsmasq servers config - if [ "$(uci_get 'dhcp' "$cfg" 'serversfile')" != "$dnsmasqServersFile" ]; then - uci_set 'dhcp' "$cfg" 'serversfile' "$dnsmasqServersFile" - fi - ;; - esac - } -# shellcheck disable=SC2317 - _dnsmasq_instance_append_force_dns_port() { - local cfg="$1" instance_port - [ -s "/etc/config/dhcp" ] || return 0 - [ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1 - config_get instance_port "$cfg" 'port' '53' - str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}" - } - _smartdns_instance_append_force_dns_port() { - local cfg="$1" instance_port - [ -s "/etc/config/smartdns" ] || return 0 - [ -n "$(uci_get 'smartdns' "$cfg")" ] || return 1 - config_get instance_port "$cfg" 'port' '53' - str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}" - } - _smartdns_instance_config() { - local cfg="$1" param="$2" - [ -s "/etc/config/smartdns" ] || return 0 - [ -n "$(uci_get 'smartdns' "$cfg")" ] || return 1 - case "$param" in - cleanup) - uci_remove_list 'smartdns' "$cfg" 'conf_files' "$outputConfig" - rm -f "$outputConfig" - ;; - smartdns.domainset) - { echo "domain-set -name adblock-fast -file $outputFile"; \ - echo "domain-rules /domain-set:adblock-fast/ -a #"; } > "$outputConfig" - uci_add_list_if_new 'smartdns' "$cfg" 'conf_files' "$outputConfig" - ;; - smartdns.ipset) - { echo "domain-set -name adblock-fast -file $outputFile"; \ - echo "domain-rules /domain-set:adblock-fast/ -ipset adb"; } > "$outputConfig" - uci_add_list_if_new 'smartdns' "$cfg" 'conf_files' "$outputConfig" - ;; - smartdns.nftset) - local nftset="#4:inet#fw4#adb4" - [ -n "$ipv6_enabled" ] && nftset="${nftset},#6:inet#fw4#adb6" - { echo "domain-set -name adblock-fast -file $outputFile"; \ - echo "domain-rules /domain-set:adblock-fast/ -nftset $nftset"; } > "$outputConfig" - uci_add_list_if_new 'smartdns' "$cfg" 'conf_files' "$outputConfig" - ;; - esac - } -# shellcheck disable=SC2317,SC2329 - _unbound_instance_append_force_dns_port() { - [ -s "/etc/config/unbound" ] || return 0 - [ -n "$(uci_get 'unbound' "$cfg")" ] || return 1 - local cfg="$1" instance_port - config_get instance_port "$cfg" 'listen_port' '53' - str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}" - } - - local i resolver_name="${dns%%.*}" - [ -z "$1" ] && return 0 - case $1 in - cleanup) - rm -f "$dnsmasqAddnhostsFile" "$dnsmasqAddnhostsCache" "${compressed_cache_dir}/${dnsmasqAddnhostsGzip}" - rm -f "$dnsmasqConfCache" "${compressed_cache_dir}/${dnsmasqConfGzip}" - rm -f "$dnsmasqIpsetCache" "${compressed_cache_dir}/${dnsmasqIpsetGzip}" - rm -f "$dnsmasqNftsetCache" "${compressed_cache_dir}/${dnsmasqNftsetGzip}" - rm -f "$dnsmasqServersFile" "$dnsmasqServersCache" "${compressed_cache_dir}/${dnsmasqServersGzip}" - rm -f "$smartdnsDomainSetFile" "$smartdnsDomainSetCache" "${compressed_cache_dir}/${smartdnsDomainSetGzip}" "$smartdnsDomainSetConfig" - rm -f "$smartdnsIpsetFile" "$smartdnsIpsetCache" "${compressed_cache_dir}/${smartdnsIpsetGzip}" "$smartdnsIpsetConfig" - rm -f "$smartdnsNftsetFile" "$smartdnsNftsetCache" "${compressed_cache_dir}/${smartdnsNftsetGzip}" "$smartdnsNftsetConfig" - rm -f "$unboundFile" "$unboundCache" "${compressed_cache_dir}/${unboundGzip}" - if [ -s "/etc/config/dhcp" ]; then - config_load 'dhcp' - config_foreach _dnsmasq_instance_config 'dnsmasq' 'cleanup' - uci_changes 'dhcp' && uci_commit 'dhcp' - fi - if [ -s "/etc/config/smartdns" ]; then - config_load 'smartdns' - config_foreach _smartdns_instance_config 'smartdns' 'cleanup' - uci_changes 'smartdns' && uci_commit 'smartdns' - fi - ;; - on_load) - : - ;; - on_stop|quiet|quiet_restart) - eval "${resolver_name}_restart" - return $? - ;; - on_start) - if ! adb_file 'test'; then - json set status 'statusFail' - json add error 'errorOutputFileCreate' "$outputFile" - return 1 - fi - output 1 "Cycling $resolver_name " - resolver 'update_config' && \ - resolver 'test' && \ - resolver 'sanity' && \ - resolver 'restart' && \ - resolver 'heartbeat' || resolver 'revert' - output 1 '\n' - ;; - test) - case "$dns" in - dnsmasq.*) - output_dns "Testing $dns configuration " - if dnsmasq --test >/dev/null 2>/dev/null; then - output_ok - return 0 - else - output_fail - return 1 - fi - ;; - smartdns.*) - return 0 - ;; - unbound.*) - return 0 - ;; - esac - ;; - restart) - output_dns "Restarting $resolver_name " - json set message "Restarting $resolver_name" - if eval "${resolver_name}_restart"; then - json set status 'statusSuccess' - led_on "$led" - output_ok - return 0 - else - output_fail - json set status 'statusFail' - json add error 'errorDNSReload' - return 1 - fi - ;; - sanity) - [ -n "$sanity_check" ] || return 0 - output_dns "Sanity check for $dns TLDs " - if ! grep -qvE '\.|server:' "$outputFile"; then - output_ok - else - json add warning 'warningSanityCheckTLD' "$outputFile" - output_warn - fi - output_dns "Sanity check for $dns leading dots " - case "$dns" in - dnsmasq.*) - if ! grep -q '/\.' "$outputFile"; then - output_ok - else - json add warning 'warningSanityCheckLeadingDot' "$outputFile" - output_warn - fi - ;; - smartdns.*) - if ! grep -q '^\.' "$outputFile"; then - output_ok - else - json add warning 'warningSanityCheckLeadingDot' "$outputFile" - output_warn - fi - ;; - unbound.*) - if ! grep -q '"\.' "$outputFile"; then - output_ok - else - json add warning 'warningSanityCheckLeadingDot' "$outputFile" - output_warn - fi - ;; - esac - ;; - heartbeat) - [ -n "$heartbeat_domain" ] || return 0 - is_integer "$heartbeat_sleep_timeout" || return 0 - output_dns "Probing $heartbeat_domain for $heartbeat_sleep_timeout seconds " - json set message "Testing resolver on $heartbeat_domain" - local i=0 - while [ "$i" -lt "$heartbeat_sleep_timeout" ]; do - if resolveip "$heartbeat_domain" >/dev/null 2>&1; then - output_ok - return 0 - fi - output_dot - i=$((i+1)) - sleep 1 - done - output_fail - json set status 'statusFail' - json add error 'errorNoHeartbeat' - return 1 - ;; - revert) - output 1 "Resetting/Restarting $resolver_name " - output_dns "Resetting $resolver_name " - resolver 'cleanup' - output_ok - output_dns "Restarting $resolver_name " - if eval "${resolver_name}_restart"; then - led_off "$led" - output_ok - return 0 - else - output_fail - json set status 'statusFail' - json add error 'errorDNSReload' - return 1 - fi - ;; - update_config) - output_dns "Updating $resolver_name configuration " - case "$dns" in - dnsmasq.*) - config_load 'dhcp' - if [ "$dnsmasq_instance" = "*" ]; then - config_foreach _dnsmasq_instance_config 'dnsmasq' "$dns" - config_foreach _dnsmasq_instance_append_force_dns_port 'dnsmasq' - elif [ -n "$dnsmasq_instance" ]; then - for i in $dnsmasq_instance; do - _dnsmasq_instance_config "@dnsmasq[$i]" "$dns" || _dnsmasq_instance_config "$i" "$dns" - _dnsmasq_instance_append_force_dns_port "@dnsmasq[$i]" || _dnsmasq_instance_append_force_dns_port "$i" - done - fi - uci_changes 'dhcp' && uci_commit 'dhcp' - if adb_file 'test'; then - chmod 660 "$outputFile" - chown root:dnsmasq "$outputFile" >/dev/null 2>/dev/null - else - json set status 'statusFail' - json add error 'errorNoOutputFile' "$outputFile" - return 1 - fi - ;; - smartdns.*) - config_load 'smartdns' - if [ "$smartdns_instance" = "*" ]; then - config_foreach _smartdns_instance_config 'smartdns' "$dns" - config_foreach _smartdns_instance_append_force_dns_port 'smartdns' - elif [ -n "$smartdns_instance" ]; then - for i in $smartdns_instance; do - _smartdns_instance_config "@smartdns[$i]" "$dns" || _smartdns_instance_config "$i" "$dns" - _smartdns_instance_append_force_dns_port "@smartdns[$i]" || _smartdns_instance_append_force_dns_port "$i" - done - fi - uci_changes 'smartdns' && uci_commit 'smartdns' - chmod 660 "$outputFile" "$outputConfig" - chown root:root "$outputFile" "$outputConfig" >/dev/null 2>/dev/null - ;; - unbound.*) - config_load 'unbound' - config_foreach _unbound_instance_append_force_dns_port 'unbound' - chmod 660 "$outputFile" - chown root:unbound "$outputFile" >/dev/null 2>/dev/null - ;; - esac - output_ok - ;; - esac -} - -adb_file() { - local R_TMP - case "$1" in - create|backup) - [ -s "$outputFile" ] && { mv -f "$outputFile" "$outputCache"; } >/dev/null 2>/dev/null - return $? - ;; - restore|use) - [ -s "$outputCache" ] && mv "$outputCache" "$outputFile" >/dev/null 2>/dev/null - return $? - ;; - test|test_file) - [ -s "$outputFile" ] - return $? - ;; - test_cache) - [ -s "$outputCache" ] - return $? - ;; - test_gzip) - [ -s "$outputGzip" ] && gzip -t -c "$outputGzip" >/dev/null 2>/dev/null - return $? - ;; - create_gzip) - [ -s "$outputFile" ] || return 1 - rm -f "$outputGzip" >/dev/null 2>/dev/null - R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")" - if gzip < "$outputFile" > "$R_TMP"; then - if mv "$R_TMP" "$outputGzip"; then - rm -f "$R_TMP" - return 0 - else - rm -f "$R_TMP" - return 1 - fi - else - return 1 - fi - ;; - expand|unpack|unpack_gzip) - [ -s "$outputGzip" ] && gzip -dc < "$outputGzip" > "$outputCache" - return $? - ;; - remove_cache) - rm -f "$outputCache" >/dev/null 2>/dev/null - ;; - remove_gzip) - rm -f "$outputGzip" >/dev/null 2>/dev/null - ;; - esac -} - -process_file_url_wrapper() { - if [ "$2" != '0' ]; then - json add error 'errorConfigValidationFail' - fi - if [ -n "$parallel_downloads" ]; then - process_file_url "$1" & - else - process_file_url "$1" - fi -} - -process_file_url() { - _sanitize_source() { - local type="$1" file="$2" - case "$type" in - hosts) - sed -i '/# Title: StevenBlack/,/# Custom host records are listed here/d' "$file" -# sed -i -E '/^(.*)[\t ](local|localhost|localhost.localdomain)$/d;/^255.255.255.255[\t ]broadcasthost$/d;/^0.0.0.0[\t ]0.0.0.0$/d' "$file" -# sed -i -E '/^(.*)[\t ](ip6-localhost|ip6-loopback|ip6-localnet|ip6-mcastprefix|ip6-allnodes|ip6-allrouters|ip6-allhosts)/d' "$file" - ;; - esac - } -# url and action are set by load_validate_file_url_section or passed as 2nd and 3rd parameter - local cfg="$1" new_size - local label type D_TMP R_TMP filter - if [ -z "$cfg" ] || [ -n "${2}${3}" ]; then - url="$2" - action="$3" - fi - - [ "$enabled" = '1' ] || return 0 - [ -n "$url" ] || return 1 - - label="${url##*//}" - label="${label%%/*}" - label="${name:-$label}" - label="List: $label" - case "$action" in - allow) type='Allowed'; D_TMP="$ALLOWED_TMP" - ;; - block) type='Blocked'; D_TMP="$B_TMP" - ;; - file) type='File'; D_TMP="$B_TMP" - ;; - esac - if is_https_url "$url" && [ -z "$isSSLSupported" ]; then - output 1 "$_FAIL_" - output 2 "[ DL ] $type $label $__FAIL__\n" - json add error 'errorNoSSLSupport' "${name:-$url}" - return 0 - fi - R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")" - if [ -z "$url" ] || ! $dl_command "$url" "$dl_flag" "$R_TMP" 2>/dev/null || \ - [ ! -s "$R_TMP" ]; then - output 1 "$_FAIL_" - output 2 "[ DL ] $type $label $__FAIL__\n" - json add error 'errorDownloadingList' "${name:-$url}" - else - append_newline "$R_TMP" - [ -n "$cfg" ] && new_size="$(get_local_filesize "$R_TMP")" - if [ -n "$new_size" ] && [ "$size" != "$new_size" ]; then - uci_set "$packageName" "$cfg" 'size' "$new_size" - fi - format="$(detect_file_type "$R_TMP")" - case "$format" in - adblockplus) filter="$adBlockPlusFilter";; - dnsmasq) filter="$dnsmasqFileFilter";; - dnsmasq2) filter="$dnsmasq2FileFilter";; - dnsmasq3) filter="$dnsmasq3FileFilter";; - domains) filter="$domainsFilter";; - hosts) - filter="$hostsFilter" - _sanitize_source 'hosts' "$R_TMP" - ;; - *) - output 1 "$_FAIL_" - output 2 "[ DL ] $type $label $__FAIL__\n" - json add error 'errorDetectingFileType' "${name:-$url}" - rm -f "$R_TMP" - return 0 - ;; - esac - if [ -n "$filter" ] && [ "$action" != 'file' ]; then - sed -i "$filter" "$R_TMP" - fi - if [ ! -s "$R_TMP" ]; then - output 1 "$_FAIL_" - output 2 "[ DL ] $type $label ($format) $__FAIL__\n" - json add error 'errorParsingList' "${name:-$url}" - else - append_newline "$R_TMP" - cat "${R_TMP}" >> "$D_TMP" - output 1 "$_OK_" - output 2 "[ DL ] $type $label ($format) $__OK__\n" - fi - fi - rm -f "$R_TMP" - return 0 -} - -download_dnsmasq_file() { - json set message "$(get_text 'statusDownloading')..." - json set status 'statusDownloading' - - rm -f "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" "$outputFile" "$outputCache" "$outputGzip" - if [ "$(get_mem_available)" -lt "$packageMemoryThreshold" ]; then - output 'Low free memory, restarting resolver ' - if resolver 'quiet_restart'; then - output_okn - else - output_failn - fi - fi - touch "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" - output 1 'Downloading dnsmasq file ' - process_file_url '' "$dnsmasq_config_file_url" 'file' - output_dns 'Moving dnsmasq file ' - if mv "$B_TMP" "$outputFile"; then - output_ok - else - output_fail - json add error 'errorMovingDataFile' "$i" - fi - output 1 '\n' -} - -download_lists() { -# shellcheck disable=SC2317,SC2329 - _ram_check() { - _config_calculate_sizes() { - local cfg="$1" - local en size url - config_get_bool en "$cfg" enabled '1' - config_get size "$cfg" size - config_get url "$cfg" url - [ "$en" = '0' ] && return 0 - [ -n "$size" ] || size="$(get_url_filesize "$url")" - [ -n "$size" ] && total_sizes=$((total_sizes+size)) - } - local i free_mem total_sizes - free_mem="$(get_mem_available)" - if [ -z "$free_mem" ]; then - json add warning 'warningFreeRamCheckFail' - output_warning "$(get_text 'warningFreeRamCheckFail')" - return 0 - fi - config_load "$packageName" - config_foreach _config_calculate_sizes 'file_url' - if [ $((free_mem)) -lt $((total_sizes * 2)) ]; then - json add error 'errorTooLittleRam' "$free_mem" - return 1 - else - return 0 - fi - } - local hf j=0 R_TMP - local step_title start_time end_time elapsed - - _ram_check || return 1 - - json set message "$(get_text 'statusDownloading')..." - json set status 'statusDownloading' - - rm -f "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" "$outputFile" "$outputCache" "$outputGzip" - if [ "$(get_mem_total)" -lt "$packageMemoryThreshold" ]; then - output 'Low free memory, restarting resolver ' - if resolver 'quiet_restart'; then - output_okn - else - output_failn - fi - fi - touch "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" - output 1 'Downloading lists ' - config_load "$packageName" - config_foreach load_validate_file_url_section 'file_url' process_file_url_wrapper - wait - if uci_changes "$packageName"; then - output 2 "[PROC] Saving updated file sizes " - if [ -n "$update_config_sizes" ] && uci_commit "$packageName"; then output_ok; else output_fail; fi - fi - output 1 '\n' - - if [ -n "$canary_domains_icloud" ]; then - canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsiCloud}" - fi - if [ -n "$canary_domains_mozilla" ]; then - canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsMozilla}" - fi - - output 1 'Processing downloads ' - - start_time=$(date +%s) - step_title='Sorting combined block-list' - output 2 "[PROC] $step_title " - json set status 'statusProcessing' - json set message "$(get_text 'statusProcessing'): $step_title" - append_newline "$B_TMP" - { - for hf in $blocked_domain $canaryDomains; do - [ -n "$hf" ] && echo "$hf" - done - } | sed "$domainsFilter" >> "$B_TMP" - sed -i '/^[[:space:]]*$/d' "$B_TMP" - [ ! -s "$B_TMP" ] && return 1 - - if [ -n "$allow_non_ascii" ]; then - if sort -u "$B_TMP" > "$A_TMP"; then - output_ok - else - output_fail - json add error 'errorSorting' - fi - else - if sort -u "$B_TMP" | grep -E -v '[^a-zA-Z0-9=/.-]' > "$A_TMP"; then - output_ok - else - output_fail - json add error 'errorSorting' - fi - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - - case "$dns" in - 'dnsmasq.conf' | 'dnsmasq.ipset' | 'dnsmasq.nftset' | 'dnsmasq.servers' | \ - 'smartdns.domainset' | 'smartdns.ipset' | 'smartdns.nftset' | \ - 'unbound.adb_list' ) - start_time=$(date +%s) - step_title='Optimizing combined block-list' - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" -# shellcheck disable=SC2016 - if $awk -F "." '{for(i=NF;i>0;i--) printf "%s%s", $i, (i>1?".":"\n")}' "$A_TMP" > "$B_TMP"; then - if sort "$B_TMP" > "$A_TMP"; then - if $awk ' - NR==1 {prev=$0; print; next} - { - len=length(prev) - if(substr($0,1,len)==prev && substr($0,len+1,1)==".") next - print - prev=$0 - } - ' "$A_TMP" > "$B_TMP"; then - if $awk -F "." '{for(i=NF;i>0;i--) printf "%s%s", $i, (i>1?".":"\n")}' "$B_TMP" > "$A_TMP"; then - if sort -u "$A_TMP" > "$B_TMP"; then - output_ok - else - output_fail - json add error 'errorOptimization' - mv "$A_TMP" "$B_TMP" - fi - else - output_fail - json add error 'errorOptimization' - fi - else - output_fail - json add error 'errorOptimization' - mv "$A_TMP" "$B_TMP" - fi - else - output_fail - json add error 'errorOptimization' - fi - else - output_fail - json add error 'errorOptimization' - mv "$A_TMP" "$B_TMP" - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - ;; - *) - mv "$A_TMP" "$B_TMP" - ;; - esac - - if [ -n "$allowed_domain" ] || [ -s "$ALLOWED_TMP" ]; then - start_time=$(date +%s) - step_title='Removing allowed domains from combined block-list' - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - local allowed_domains_from_dl - [ -s "$ALLOWED_TMP" ] && allowed_domains_from_dl="$(sed '/^[[:space:]]*$/d' "$ALLOWED_TMP")" - allowed_domain="${allowed_domain}${allowed_domains_from_dl:+ $allowed_domains_from_dl}" - for hf in ${allowed_domain}; do - hf="$(echo "$hf" | sed 's/\./\\./g')" - echo "/(^|\.)${hf}$/d;" - done > "$SED_TMP" - # if only doing exact matches, may be faster to add $hf to $ALLOWED_TMP and then - # grep -vFf "$ALLOWED_TMP" "$B_TMP" > "$A_TMP" && mv "$A_TMP" "$B_TMP" - if [ -s "$SED_TMP" ]; then - if sed -E -f "$SED_TMP" "$B_TMP" > "$A_TMP" && mv "$A_TMP" "$B_TMP"; then - output_ok - else - output_fail - json add error 'errorAllowListProcessing' - fi - else - output_fail - json add error 'errorAllowListProcessing' - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - fi - - start_time=$(date +%s) - step_title='Formatting combined block-list file' - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - if [ -z "$outputFilterIPv6" ]; then - if sed "$outputFilter" "$B_TMP" > "$A_TMP"; then - output_ok - else - output_fail - json add error 'errorDataFileFormatting' - fi - else - case "$dns" in - dnsmasq.addnhosts) - if sed "$outputFilter" "$B_TMP" > "$A_TMP" && \ - sed "$outputFilterIPv6" "$B_TMP" >> "$A_TMP"; then - output_ok - else - output_fail - json add error 'errorDataFileFormatting' - fi - ;; - esac - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - - if [ -n "$outputAllowFilter" ] && [ -n "$allowed_domain" ]; then - rm -f "$SED_TMP"; touch "$SED_TMP"; - start_time=$(date +%s) - step_title="Explicitly allowing domains in ${dns}" - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - for hf in ${allowed_domain}; do - echo "$hf" | sed -E "$outputAllowFilter" >> "$SED_TMP" - done - if [ -s "$SED_TMP" ]; then - if cat "$SED_TMP" "$A_TMP" > "$B_TMP"; then - output_ok - else - output_fail - json add error 'errorAllowListProcessing' - fi - else - output_fail - json add error 'errorAllowListProcessing' - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - else - mv "$A_TMP" "$B_TMP" - fi - - start_time=$(date +%s) - step_title="Setting up ${dns} file" - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - - case "$dns" in - unbound.adb_list) - if mv "$B_TMP" "$outputFile"; then - output_ok - else - output_fail - json add error 'errorMovingDataFile' "$outputFile" - fi - sed -i '1 i\server:' "$outputFile" - ;; - *) - if mv "$B_TMP" "$outputFile"; then - output_ok - else - output_fail - json add error 'errorMovingDataFile' "$outputFile" - fi - ;; - esac - - # Validate and remove invalid domain entries (RFC 1123 compliant) - if [ -n "$dnsmasq_validity_check" ]; then - case "$dns" in - dnsmasq.conf|dnsmasq.ipset|dnsmasq.nftset|dnsmasq.servers|dnsmasq.addnhosts) - start_time=$(date +%s) - step_title='Validating domain entries' - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - invalid_file="/tmp/${packageName}.invalid.tmp" - rm -f "$invalid_file" - # Fast validation: remove entries where domain: - # - starts with dash or dot (invalid per RFC) - # - is all numeric with dots (IP-like, invalid for domain) - # - has consecutive dots - # - ends with dash or dot (invalid per RFC) - sed "$outputParseFilter" "$outputFile" | \ - grep -E '^-|^\.|^[0-9.]+$|\.\.|-$|\.$' > "$invalid_file" 2>/dev/null || true - if [ -s "$invalid_file" ]; then - invalid_count=$(wc -l < "$invalid_file" 2>/dev/null || echo 0) - if [ "$invalid_count" -gt 0 ]; then - # Create pattern file for grep -vFf (fastest removal method) - # Use appropriate prefix based on dns type - case "$dns" in - dnsmasq.conf) - sed "$dnsmasqConfGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null - ;; - dnsmasq.ipset) - sed "$dnsmasqIpsetGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null - ;; - dnsmasq.nftset) - sed "$dnsmasqNftsetGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null - ;; - dnsmasq.servers) - sed "$dnsmasqServersGrepPattern" "$invalid_file" > "${invalid_file}.pat" 2>/dev/null - ;; - dnsmasq.addnhosts) - # Create patterns for both IPv4 and IPv6 formats - { sed "$dnsmasqAddnhostsGrepPatternIPv4" "$invalid_file"; sed "$dnsmasqAddnhostsGrepPatternIPv6" "$invalid_file"; } > "${invalid_file}.pat" 2>/dev/null - ;; - esac - # Remove invalid entries - grep -vFf "${invalid_file}.pat" "$outputFile" > "${outputFile}.valid" 2>/dev/null && \ - mv "${outputFile}.valid" "$outputFile" 2>/dev/null - # Report (limit to first 20 for performance) - logger -t "$packageName" "Removed $invalid_count invalid entries from ${dns}." - json add warning 'warningInvalidDomainsRemoved' "$invalid_count" - rm -f "${invalid_file}.pat" - fi - rm -f "$invalid_file" - fi - if [ "${invalid_count:-0}" -gt 0 ]; then - output_warn - else - output_ok - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - ;; - esac - fi - - step_title='Removing temporary files' - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - if rm -f "/tmp/${packageName}_tmp."* "$ALLOWED_TMP" "$A_TMP" "$B_TMP" "$SED_TMP" "$outputCache"; then - output_ok - else - output_fail - json add error 'errorRemovingTempFiles' - fi - output 1 '\n' -} - -adb_config_update() { -# shellcheck disable=SC2317,SC2329 - _cleanup_missing_urls() { - local cfg="$1" url size - config_get url "$cfg" url - if [ -z "$url" ]; then - uci_delete "$packageName" "$cfg" - fi - } - local R_TMP label - local param="${1:-quiet}" - load_package_config - load_dl_command - label="${config_update_url##*//}" - label="${label%%/*}"; - [ -n "$enabled" ] || return 0 - [ -n "$config_update_enabled" ] || return 0 - - if [ "$param" != 'download' ]; then - adb_file 'test' && return 0 - adb_file 'test_cache' && return 0 - adb_file 'test_gzip' && return 0 - fi - - output 1 'Updating config ' - output 2 "[ DL ] Config Update: $label " - R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")" - if ! $dl_command "$config_update_url" "$dl_flag" "$R_TMP" 2>/dev/null || [ ! -s "$R_TMP" ]; then - append_newline "$R_TMP" - output_failn - json add error 'errorDownloadingConfigUpdate' - else - if [ -s "$R_TMP" ] && sed -f "$R_TMP" -i "$packageConfigFile" 2>/dev/null; then - output_okn - else - output_failn - json add error 'errorParsingConfigUpdate' - fi - fi - rm -f "$R_TMP" - config_load "$packageName" - config_foreach _cleanup_missing_urls 'file_url' - uci_changes "$packageName" && uci_commit "$packageName" - return 0 -} - -# shellcheck disable=SC2120 +boot() { rc_procd start_service 'on_boot' && service_started 'on_boot'; } start_service() { - local status error param="${1:-on_start}" - local action p iface k - status="$(json get status)" - error="$(json get error)" - json del all - - case "$param" in - on_boot) - if adb_file 'test_gzip' || adb_file 'test_cache'; then - unset adbf_boot_flag - else - return 0 - fi - ;; - esac - - adb_config_update "$param" - load_environment "$param" "$(load_validate_config)" || return 1 - - action="$(adb_config_cache get trigger_service)" - fw4_restart_flag="$(adb_config_cache get trigger_fw4)" - - if [ -n "$error" ]; then - action='download' - elif ! adb_file 'test'; then - if adb_file 'test_gzip' || adb_file 'test_cache'; then - action='restore' - else - action='download' - fi - elif [ "$status" = "statusSuccess" ]; then - action='skip' - fi - - case "${action}:${param}" in - on_boot:*|*:on_boot|*:on_pause) - if adb_file 'test_gzip' || adb_file 'test_cache'; then - action='restore' - else - action='download' - fi - ;; - download:*|*:download) - action='download';; - restart:*) - action='restart';; - restore:*) - action='restore';; - skip:*) - action='skip';; - *:*) - action='download';; - esac - - if [ "$action" = 'restore' ]; then - output 1 "Starting $serviceName...\n" - output 2 "[INIT] Starting $serviceName...\n" - json set status 'statusStarting' - if adb_file 'test_gzip' && ! adb_file 'test_cache' && ! adb_file 'test'; then - output 1 'Found compressed cache file, unpacking it ' - output 2 '[INIT] Found compressed cache file, unpacking it ' - json set message 'found compressed cache file, unpacking it.' - if adb_file 'unpack_gzip'; then - output_okn - else - output_failn - output_error "$(get_text 'errorRestoreCompressedCache')" - action='download' - fi - fi - if adb_file 'test_cache' && ! adb_file 'test'; then - output 1 'Found cache file, reusing it ' - output 2 '[INIT] Found cache file, reusing it ' - json set message 'found cache file, reusing it.' - if adb_file 'restore'; then - unset sanity_check - unset heartbeat_domain - output_okn - resolver 'on_start' - else - output_failn - output_error "$(get_text 'errorRestoreCache')" - action='download' - fi - fi - fi - - if [ "$action" = 'download' ]; then - if [ -z "$blocked_url" ] && [ -z "$blocked_domain" ]; then - json set status 'statusFail' - json add error 'errorNothingToDo' - else - if ! adb_file 'test' || adb_file 'test_cache' || adb_file 'test_gzip'; then - output 1 "Force-reloading $serviceName...\n" - output 2 "[INIT] Force-reloading $serviceName...\n" - json set status 'statusForceReloading' - else - output 1 "Starting $serviceName...\n" - output 2 "[INIT] Starting $serviceName...\n" - json set status 'statusStarting' - fi - resolver 'cleanup' - if [ "$dns" = 'dnsmasq.conf' ] && [ -n "$dnsmasq_config_file_url" ]; then - download_dnsmasq_file - else - download_lists - fi - resolver 'on_start' - fi - fi - - if [ "$action" = 'restart' ]; then - output 1 "Restarting $serviceName...\n" - output 2 "[INIT] Restarting $serviceName...\n" - json set status 'statusRestarting' - unset sanity_check - unset heartbeat_domain - resolver 'on_start' - fi - - if [ "$action" = 'start' ]; then - output 1 "Starting $serviceName...\n" - output 2 "[INIT] Starting $serviceName...\n" - json set status 'statusStarting' - unset sanity_check - unset heartbeat_domain - resolver 'on_start' - fi - - if adb_file 'test' && [ "$(json get status)" != "statusFail" ]; then - json del message - json set status 'statusSuccess' - json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})" - status_service 'on_start_success' - else - json set status 'statusFail' - json add error 'errorOhSnap' - status_service 'on_start_failure' - resolver 'revert' - fi - - adb_config_cache 'create' - - procd_open_instance 'main' - procd_set_param command /bin/true - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_open_data - json_add_string 'version' "$PKG_VERSION" - json_add_string 'status' "$(json get status)" - json_add_int 'packageCompat' "$packageCompat" - json_add_int 'entries' "$(count_blocked_domains)" - json_add_array 'errors' - for k in $(json get errors); do - json_add_object "$k" - json_add_string 'code' "$(json get error "$k" 'code')" - json_add_string 'info' "$(json get error "$k" 'info')" - json_close_object - done - json_close_array - json_add_array 'warnings' - for k in $(json get warnings); do - json_add_object "$k" - json_add_string 'code' "$(json get warning "$k" 'code')" - json_add_string 'info' "$(json get warning "$k" 'info')" - json_close_object - done - json_close_array - json_add_array firewall - if [ -n "$force_dns" ]; then -# shellcheck disable=SC3060 - for p in ${force_dns_port/,/ }; do - if is_port_listening "$p"; then - for iface in $force_dns_interface; do - json_add_object '' - json_add_string type 'redirect' - json_add_string target 'DNAT' - json_add_string src "$iface" - json_add_string proto 'tcp udp' - json_add_string src_dport '53' - json_add_string dest_port "$p" - json_add_string family 'any' - json_add_boolean reflection '0' - json_close_object - done - else - for iface in $force_dns_interface; do - json_add_object '' - json_add_string type 'rule' - json_add_string src "$iface" - json_add_string dest '*' - json_add_string proto 'tcp udp' - json_add_string dest_port "$p" - json_add_string target 'REJECT' - json_close_object - done - fi - done - fi - case "$dns" in - dnsmasq.ipset|smartdns.ipset) - json_add_object '' - json_add_string type 'ipset' - json_add_string name 'adb' - json_add_string match 'dest_net' - json_add_string storage 'hash' - json_close_object - for iface in $force_dns_interface; do - json_add_object '' - json_add_string type 'rule' - json_add_string ipset 'adb' - json_add_string src "$iface" - json_add_string dest '*' - json_add_string proto 'tcp udp' - json_add_string target 'REJECT' - json_close_object - done - ;; - dnsmasq.nftset|smartdns.nftset) - json_add_object '' - json_add_string type 'ipset' - json_add_string name 'adb4' - json_add_string family '4' - json_add_string match 'dest_net' - json_close_object - for iface in $force_dns_interface; do - json_add_object '' - json_add_string type 'rule' - json_add_string ipset 'adb4' - json_add_string src "$iface" - json_add_string dest '*' - json_add_string proto 'tcp udp' - json_add_string target 'REJECT' - json_close_object - done - if [ -n "$ipv6_enabled" ]; then - json_add_object '' - json_add_string type 'ipset' - json_add_string name 'adb6' - json_add_string family '6' - json_add_string match 'dest_net' - json_close_object - for iface in $force_dns_interface; do - json_add_object '' - json_add_string type 'rule' - json_add_string ipset 'adb6' - json_add_string src "$iface" - json_add_string dest '*' - json_add_string proto 'tcp udp' - json_add_string target 'REJECT' - json_close_object - done - fi - ;; - esac - json_close_array - procd_close_data - procd_close_instance - return 0 -} - -status_service() { - local param="$1" - local c status message error warning stats text - local code info - load_package_config - status="$(json get status)" - message="$(json get message)" - error="$(json get error)" - warning="$(json get warning)" - stats="$(json get stats)" - if [ "$status" = "statusSuccess" ]; then - output 1 "* $stats\n" - output 2 "[STAT] $stats\n" - else - [ -n "$status" ] && status="$(get_text "$status")" - status="${status}${status:+${message:+: $message}}" - case "$(adb_file 'test_cache'; echo $?:$(adb_file 'test_gzip'; echo $?))" in - "0:0") - message="cache file and compressed cache file found" - ;; - "0:1") - message="cache file found" - ;; - "1:0") - message="compressed cache file found" - ;; - *) - unset message - ;; - esac - status="${status}${status:+${message:+ ($message)}}" - [ -n "$status" ] && output "$serviceName $status.\n" - fi - [ "$param" != 'quiet' ] || return 0 - if [ -n "$error" ]; then - for c in $error; do - code="$(json get error "$c" 'code')" - info="$(json get error "$c" 'info')" - output_error "$(get_text "$code" "$info")" - done - fi - if [ -n "$warning" ]; then - for c in $warning; do - code="$(json get warning "$c" 'code')" - info="$(json get warning "$c" 'info')" - output_warning "$(get_text "$code" "$info")" - done - fi -} - -# shellcheck disable=SC2120 -stop_service() { - load_package_config - if adb_file 'test'; then - output 1 "Stopping $serviceName... " - output 2 "[STOP] Stopping $serviceName... " - adb_file 'create' - if resolver 'on_stop'; then - ipset -q -! flush adb > /dev/null 2>&1 - ipset -q -! destroy adb > /dev/null 2>&1 - nft delete set inet fw4 adb4 > /dev/null 2>&1 - nft delete set inet fw4 adb6 > /dev/null 2>&1 - led_off "$led" - output_okn - json set status 'statusStopped' - json del message - else - output_failn; - json set status 'statusFail' - json add error 'errorStopping' - output_error "$(get_text 'errorStopping')" - fi - fi - return 0 -} - -boot() { -# ubus -t 30 wait_for network.interface 2>/dev/null - adbf_boot_flag=1 - rc_procd start_service 'on_boot' && service_started 'on_boot' + local param="${1:-on_start}" + _procd_svc_data="$($_ucode start "$param")" } +service_data() { [ -n "$_procd_svc_data" ] && eval "$_procd_svc_data"; } +stop_service() { eval "$($_ucode stop)"; } reload_service() { rc_procd start_service 'reload'; } restart_service() { rc_procd start_service 'restart'; } -service_stopped() { is_fw4_restart_needed && procd_set_config_changed firewall; } +status_service() { $_ucode status "$@"; } +service_stopped() { [ "$_fw4_restart" = 1 ] && procd_set_config_changed firewall; } +service_started() { [ "$_fw4_restart" = 1 ] && procd_set_config_changed firewall; } service_triggers() { - local wan wan6 i - if [ -n "$adbf_boot_flag" ]; then - output 1 'Setting trigger (on_boot) ' - output 2 '[TRIG] Setting trigger (on_boot) ' - procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" start && output_okn || output_failn - triggerStatus='statusTriggerBootWait' - else - procd_open_validate - load_validate_file_url_section - procd_close_validate + local wan wan6 procd_trigger_wan6 i + if [ ! -s "/dev/shm/${packageName}" ]; then + procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" start + else network_flush_cache network_find_wan wan wan="${wan:-wan}" - if [ -n "$procd_trigger_wan6" ]; then + procd_trigger_wan6="$(uci -q get "${packageName}.config.procd_trigger_wan6")" + if [ "$procd_trigger_wan6" = '1' ]; then network_find_wan6 wan6 wan6="${wan6:-wan6}" fi - output 1 "Setting trigger${wan6:+s} for $wan ${wan6:+$wan6 }" - output 2 "[TRIG] Setting trigger${wan6:+s} for $wan ${wan6:+$wan6 }" for i in $wan $wan6; do - procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" start && output_ok || output_fail + procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" start "on_${i}" done - output 1 '\n' procd_add_config_trigger "config.change" "$packageName" "/etc/init.d/${packageName}" reload - triggerStatus='statusTriggerStartWait' + procd_open_validate + load_validate_config + load_validate_file_url_section + procd_close_validate fi } - -service_started() { - local start_time end_time elapsed step_title - if [ -n "$compressed_cache" ] && ! adb_file 'test_gzip' && adb_file 'test'; then - start_time=$(date +%s) - step_title="Creating ${dns} compressed cache" - output 1 "${step_title} " - output 2 "[PROC] ${step_title} " - json set message "$(get_text 'statusProcessing'): ${step_title}" - if adb_file 'create_gzip'; then - output_okn - else - output_failn - json add error 'errorCreatingCompressedCache' - fi - end_time=$(date +%s) - elapsed=$(( end_time - start_time )) - logger_debug "[PERF-DEBUG] ${step_title} took ${elapsed}s" - else - adb_file 'remove_gzip' - fi - is_fw4_restart_needed && procd_set_config_changed firewall - [ -z "$(json get status)" ] && json set status "$triggerStatus" -} - -allow() { - local c hf string="$1" - load_package_config - if ! adb_file 'test'; then - output "No block-list ('$outputFile') found.\n" - return 0 - elif [ -z "$string" ]; then - output "Usage: /etc/init.d/${packageName} allow 'domain' ...\n" - return 0 - elif [ -n "$dnsmasq_config_file_url" ]; then - output "Allowing individual domains is not possible when using external dnsmasq config file.\n" - return 0 - fi - case "$dns" in - dnsmasq.*) - output 1 'Allowing domains and restarting dnsmasq ' - output 2 '[PROC] Allowing domains \n' - for c in $string; do - output 2 " $c " - hf="$(echo "$c" | sed 's/\./\\./g')" - if sed -i "\:\(/\|\.\)${hf}/:d" "$outputFile"; then - output_ok - else - output_fail - fi - if [ -n "$outputAllowFilter" ]; then - if echo "$c" | sed -E "$outputAllowFilter" >> "$outputFile"; then - output_ok - else - output_fail - fi - fi - if uci_add_list_if_new "${packageName}" 'config' 'allowed_domain' "$c"; then - output_ok - else - output_fail - fi - done - if [ -n "$compressed_cache" ]; then - output 2 '[PROC] Creating compressed cache ' - if adb_file 'create_gzip'; then - output_ok - else - output_fail - fi - fi - output 2 '[PROC] Committing changes to config ' - if uci_commit "$packageName"; then - allowed_domain="$(uci_get "$packageName" 'config' 'allowed_domain')" - adb_config_cache 'create' - json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})" - output_ok - if [ "$dns" = 'dnsmasq.ipset' ]; then - output 2 '[PROC] Flushing adb ipset ' - if ipset -q -! flush adb; then output_ok; else output_fail; fi - fi - if [ "$dns" = 'dnsmasq.nftset' ]; then - output 2 '[PROC] Flushing adb nft sets ' - nft flush set inet fw4 adb6 - if nft flush set inet fw4 adb4; then output_ok; else output_fail; fi - fi - output_dns 'Restarting dnsmasq ' - if dnsmasq_restart; then output_ok; else output_fail; fi - else - output_fail - fi - output 1 '\n' - ;; - smartdns.*) - output 1 'Allowing domains and restarting smartdns ' - output 2 '[PROC] Allowing domains \n' - for c in $string; do - output 2 " $c " - hf="$(echo "$c" | sed 's/\./\\./g')" - if sed -i "\:\(\"\|\.\)${hf}\":d" "$outputFile" && \ - uci_add_list_if_new "$packageName" 'config' 'allowed_domain' "$string"; then - output_ok - else - output_fail - fi - done - if [ -n "$compressed_cache" ]; then - output 2 '[PROC] Creating compressed cache ' - if adb_file 'create_gzip'; then - output_ok - else - output_fail - fi - fi - output 2 '[PROC] Committing changes to config ' - if uci_commit "$packageName"; then - allowed_domain="$(uci_get "$packageName" 'config' 'allowed_domain')" - adb_config_cache 'create' - json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})" - output_ok; - output_dns 'Restarting SmartDNS ' - if smartdns_restart; then output_ok; else output_fail; fi - else - output_fail - fi - output 1 '\n' - ;; - unbound.*) - output 1 'Allowing domains and restarting Unbound ' - output 2 '[PROC] Allowing domains \n' - for c in $string; do - output 2 " $c " - hf="$(echo "$c" | sed 's/\./\\./g')" - if sed -i "\:\(\"\|\.\)${hf}\":d" "$outputFile" && \ - uci_add_list_if_new "$packageName" 'config' 'allowed_domain' "$string"; then - output_ok - else - output_fail - fi - done - if [ -n "$compressed_cache" ]; then - output 2 '[PROC] Creating compressed cache ' - if adb_file 'create_gzip'; then - output_ok - else - output_failn - fi - fi - output 2 '[PROC] Committing changes to config ' - if uci_commit "$packageName"; then - allowed_domain="$(uci_get "$packageName" 'config' 'allowed_domain')" - adb_config_cache 'create' - json set stats "$serviceName is blocking $(count_blocked_domains) domains (with ${dns})" - output_ok; - output_dns 'Restarting Unbound ' - if unbound_restart; then output_ok; else output_fail; fi - else - output_fail - fi - output 1 '\n' - ;; - esac -} - -check() { - local c param="$1" - load_package_config - if ! adb_file 'test'; then - output "No block-list ('$outputFile') found.\n" - return 0 - elif [ -z "$param" ]; then - output "Usage: /etc/init.d/${packageName} check 'domain' ...\n" - return 0 - fi - for string in ${param}; do - c="$(grep -c -E "$string" "$outputFile")" - if [ "$c" -gt 0 ]; then - if [ "$c" -eq 1 ]; then - output 1 "Found 1 match for '$string' in '$outputFile'.\n" - output 2 "[PROC] Found 1 match for '$string' in '$outputFile'.\n" - else - output 1 "Found $c matches for '$string' in '$outputFile'.\n" - output 2 "[PROC] Found $c matches for '$string' in '$outputFile'.\n" - fi - if [ "$c" -le 20 ]; then - grep "$string" "$outputFile" | sed "$outputParseFilter" - fi - else - output 1 "The '$string' is not found in current block-list ('$outputFile').\n" - output 2 "[PROC] The '$string' is not found in current block-list ('$outputFile').\n" - fi - done -} - -check_tld() { - local c param="$1" - load_package_config - if ! adb_file 'test'; then - output "No block-list ('$outputFile') found.\n" - return 0 - fi - c="$(grep -cvE '\.|server:' "$outputFile")" - if [ "$c" -gt 0 ]; then - if [ "$c" -eq 1 ]; then - output 1 "Found 1 match for TLD in '$outputFile'.\n" - output 2 "[PROC] Found 1 match for TLD in '$outputFile'.\n" - else - output 1 "Found $c matches for TLDs in '$outputFile'.\n" - output 2 "[PROC] Found $c matches for TLDs in '$outputFile'.\n" - fi - if [ "$c" -le 20 ]; then - grep -vE '\.|server:' "$outputFile" | sed "$outputParseFilter" - fi - else - output 1 "No TLD was found in current block-list ('$outputFile').\n" - output 2 "[PROC] No TLD was found in current block-list ('$outputFile').\n" - fi -} - -check_leading_dot() { - local c param="$1" - local string - load_package_config - if ! adb_file 'test'; then - output "No block-list ('$outputFile') found.\n" - return 0 - fi - case "$dns" in - dnsmasq.*) string='/\.';; - smartdns.*) string='^\.';; - unbound.*) string='"\.';; - esac - c="$(grep -c "$string" "$outputFile")" - if [ "$c" -gt 0 ]; then - if [ "$c" -eq 1 ]; then - output 1 "Found 1 match for leading-dot domain in '$outputFile'.\n" - output 2 "[PROC] Found 1 match for leading-dot domain in '$outputFile'.\n" - else - output 1 "Found $c matches for leading-dot domains in '$outputFile'.\n" - output 2 "[PROC] Found $c matches for leading-dot domains in '$outputFile'.\n" - fi - if [ "$c" -le 20 ]; then - grep "$string" "$outputFile" | sed "$outputParseFilter" - fi - else - output 1 "No leading-dot domain was found in current block-list ('$outputFile').\n" - output 2 "[PROC] No leading-dot domain was found in current block-list ('$outputFile').\n" - fi -} - -check_lists() { -# shellcheck disable=SC2317,SC2329 - _check_list() { - local cfg="$1" - local en size url name R_TMP string c - config_get_bool en "$cfg" enabled '1' - config_get action "$cfg" action 'block' - config_get url "$cfg" url - config_get name "$cfg" name - name="${name:-$url}" - - [ "$en" = '0' ] && return 0 - [ "$action" != 'block' ] && return 0 - - output 1 "Checking ${name}: " - output 2 "[ DL ] $name " - - if is_https_url "$url" && [ -z "$isSSLSupported" ]; then - output_failn - return 1 - fi - R_TMP="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")" - if [ -z "$url" ] || ! $dl_command "$url" "$dl_flag" "$R_TMP" 2>/dev/null || \ - [ ! -s "$R_TMP" ]; then - output_failn - return 1 - else - output 2 "$__OK__\n" - fi - append_newline "$R_TMP" - for string in ${param}; do - c="$(grep -c -E "$string" "$R_TMP")" - if [ "$c" -gt 0 ]; then - if [ "$c" -eq 1 ]; then - output 1 "found 1 match for '$string'.\n" - output 2 "[PROC] Found 1 match for '$string' in '$url'.\n" - else - output 1 "found $c matches for '$string'.\n" - output 2 "[PROC] Found $c matches for '$string' in '$url'.\n" - fi - grep "$string" "$R_TMP" - else - output 1 "'$string' not found.\n" - output 2 "[PROC] The '$string' is not found in '$url'.\n" - fi - done - rm -f "$R_TMP" - } - local param="$1" - load_package_config - load_dl_command - if [ -z "$param" ]; then - output "Usage: /etc/init.d/${packageName} check_lists 'domain' ...\n" - return 0 - fi - config_load "$packageName" - config_foreach _check_list 'file_url' - return 0 -} - -dl() { rc_procd start_service 'download' && service_started 'download'; } - -killcache() { - load_package_config - rm -f "$dnsmasqAddnhostsCache" "${compressed_cache_dir}/${dnsmasqAddnhostsGzip}" - rm -f "$dnsmasqConfCache" "${compressed_cache_dir}/${dnsmasqConfGzip}" - rm -f "$dnsmasqIpsetCache" "${compressed_cache_dir}/${dnsmasqIpsetGzip}" - rm -f "$dnsmasqNftsetCache" "${compressed_cache_dir}/${dnsmasqNftsetGzip}" - rm -f "$dnsmasqServersCache" "${compressed_cache_dir}/${dnsmasqServersGzip}" - rm -f "$smartdnsDomainSetCache" "${compressed_cache_dir}/${smartdnsDomainSetGzip}" - rm -f "$smartdnsIpsetCache" "${compressed_cache_dir}/${smartdnsIpsetGzip}" - rm -f "$smartdnsNftsetCache" "${compressed_cache_dir}/${smartdnsNftsetGzip}" - rm -f "$unboundCache" "${compressed_cache_dir}/${unboundGzip}" - resolver 'cleanup' - return 0 -} - -pause() { - load_package_config - local timeout="${1:-$pause_timeout}" - stop_service 'on_pause' - output 1 "Sleeping for $timeout seconds... " - output 2 "[PROC] Sleeping for $timeout seconds... " - if is_integer "$timeout" && sleep "$timeout"; then - output_okn - else - output_failn - fi - start_service 'on_pause' -} - -show_blocklist() { - load_package_config - sed "$stripToDomainsFilter" "$outputFile" -} - -sizes() { -# shellcheck disable=SC2329 - _config_add_url_size() { - local cfg="$1" url name size - config_get url "$cfg" url - config_get name "$cfg" name - size="$(get_url_filesize "$url")" - output "${name:-$url}${size:+: $size} " - if [ -n "$size" ]; then - uci_set "$packageName" "$cfg" 'size' "$size" - output_okn - else - output_failn - fi - } - local i - load_package_config - load_dl_command - config_load "$packageName" - config_foreach _config_add_url_size 'file_url' - [ -n "$update_config_sizes" ] && uci_changes "$packageName" && uci_commit "$packageName" -} - -version() { echo "$PKG_VERSION"; } +allow() { $_ucode allow "$@"; } +check() { $_ucode check "$@"; } +check_tld() { $_ucode check_tld "$@"; } +check_leading_dot() { $_ucode check_leading_dot "$@"; } +check_lists() { $_ucode check_lists "$@"; } +dl() { rc_procd start_service 'download' && service_started; } +killcache() { $_ucode killcache "$@"; } +pause() { $_ucode pause "$@"; } +show_blocklist() { $_ucode show_blocklist "$@"; } +sizes() { $_ucode sizes "$@"; } +version() { $_ucode version "$@"; } # shellcheck disable=SC2120 load_validate_file_url_section() { - uci_load_validate "$packageName" "$packageName" "$1" "$2" \ + uci_load_validate "$packageName" 'file_url' "$1" "$2" \ 'enabled:bool:1' \ 'action:or("allow", "block"):block' \ 'size:or(uinteger, "")' \ @@ -2703,8 +89,9 @@ load_validate_file_url_section() { ; } +# shellcheck disable=SC2120 load_validate_config() { - uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ $3}" \ + uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ ${3}}" \ 'enabled:bool:0' \ 'force_dns:bool:1' \ 'force_dns_interface:list(network):lan' \ @@ -2730,8 +117,8 @@ load_validate_config() { 'procd_boot_wan_timeout:integer:60' \ 'led:or("", "none", file, device, string)' \ 'dns:or("dnsmasq.addnhosts", "dnsmasq.conf", "dnsmasq.ipset", "dnsmasq.nftset", "dnsmasq.servers", "smartdns.domainset", "smartdns.ipset", "smartdns.nftset", "unbound.adb_list"):dnsmasq.servers' \ - 'dnsmasq_instance:list(or(integer, string)):*' \ - 'smartdns_instance:list(or(integer, string)):*' \ + 'dnsmasq_instance:list(or("*", "-", uinteger, uci("dhcp", "@dnsmasq"))):*' \ + 'smartdns_instance:list(or("*", "-", uinteger, uci("smartdns", "@smartdns"))):*' \ 'heartbeat_domain:or("-", string):heartbeat.melmac.ca' \ 'heartbeat_sleep_timeout:range(1,60):10' \ 'dnsmasq_sanity_check:bool:1' \ diff --git a/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast b/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast index 2414540029..0e2cd03312 100644 --- a/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast +++ b/net/adblock-fast/files/etc/uci-defaults/90-adblock-fast @@ -1,181 +1,65 @@ #!/bin/sh -# Copyright 2023 MOSSDeF, Stan Grishin (stangri@melmac.ca) -# shellcheck disable=SC2015,SC3043,SC3060 +# Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca) +# shellcheck disable=SC2015,SC3043 +readonly pkg='adblock-fast' -readonly adbFunctionsFile='/etc/init.d/adblock-fast' -if [ -s "$adbFunctionsFile" ]; then -# shellcheck source=../../etc/init.d/adblock-fast - . "$adbFunctionsFile" -else - printf "%b: adblock-fast init.d file (%s) not found! \n" '\033[0;31mERROR\033[0m' "$adbFunctionsFile" -fi +# ── Transition to list names ───────────────────────────────────────── +# Adds 'name' to file_url sections that lack one, using the pristine default config -# Transition from simple-adblock -_enable_url() { - local cfg="$1" url="$2" action="$3" - local u a - config_get u "$cfg" 'url' - config_get a "$cfg" 'action' 'block' - if [ "$u" = "$url" ] && [ "$a" = "$action" ]; then - uci_remove "$packageName" "$cfg" 'enabled' && _found=1 - fi -} +# Find pristine default: apk uses .apk-new, opkg uses -opkg +pristine='' +for f in "/etc/config/${pkg}.apk-new" "/etc/config/${pkg}-opkg"; do + [ -s "$f" ] && pristine="$f" && break +done -enable_add_url() { - local url="$1" action="$2" _found - config_load "$packageName" - config_foreach _enable_url 'file_url' "$url" "$action" - if [ -z "$_found" ]; then - uci_add "$packageName" 'file_url' - uci_set "$packageName" '@file_url[-1]' 'url' "$url" - uci_set "$packageName" '@file_url[-1]' 'size' "$(get_url_filesize "$url")" - uci_set "$packageName" '@file_url[-1]' 'action' "$action" - fi -} +_find_name() { grep -B1 "$1" "$pristine" 2>/dev/null | head -1 | cut -d "'" -f2; } -if [ -s '/etc/config/simple-adblock' ] \ - && [ ! -s '/etc/config/adblock-fast-opkg' ] \ - && [ "$(uci_get adblock-fast config enabled)" = '0' ]; then - cp -f '/etc/config/adblock-fast' '/etc/config/adblock-fast-opkg' - enabled="$(uci_get simple-adblock config enabled)" - if [ -x '/etc/init.d/simple-adblock' ]; then - output "Stopping and disabling simple-adblock " - if /etc/init.d/simple-adblock stop >/dev/null 2>&1 \ - && /etc/init.d/simple-adblock disable \ - && uci_set simple-adblock config enabled 0 \ - && uci_commit simple-adblock; then - output_okn - else - output_failn - fi - else - output "Disabling simple-adblock." - if uci_set simple-adblock config enabled 0 \ - && uci_commit simple-adblock; then - output_okn - else - output_failn - fi - fi - output "Migrating simple-adblock config file " - for i in allow_non_ascii canary_domains_icloud canary_domains_mozilla \ - compressed_cache compressed_cache_dir config_update_enabled \ - curl_additional_param curl_max_file_size curl_retry download_timeout \ - debug dns dns_instance dnsmasq_config_file_url force_dns led \ - parallel_downloads procd_trigger_wan6 procd_boot_wan_timeout verbosity; do - j="$(uci_get simple-adblock.config.${i})" - [ -n "$j" ] && uci_set "$packageName" config "$i" "$j" - done - [ -n "$enabled" ] && uci_set "$packageName" config enabled "$enabled" - j="$(uci_get simple-adblock config config_update_url)" - if [ "${j//simple-adblock/}" = "$j" ]; then - uci_set "$packageName" config config_update_url "$j" - fi - ccd="$(uci_get simple-adblock config compressed_cache_dir '/etc')" - for j in $(uci_get simple-adblock config allowed_domain); do - [ -n "$j" ] && uci_add_list "$packageName" config allowed_domain "$j" - done - for j in $(uci_get simple-adblock config blocked_domain); do - [ -n "$j" ] && uci_add_list "$packageName" config blocked_domain "$j" - done - for j in $(uci_get simple-adblock config force_dns_port); do - [ -n "$j" ] && uci_add_list "$packageName" config force_dns_port "$j" - done - output_okn - - for i in allowed_domains_url blocked_adblockplus_url blocked_domains_url \ - blocked_hosts_url; do - output "Migrating simple-adblock ${i} " - for j in $(uci_get simple-adblock config "$i"); do - if [ "$i" = 'allowed_domains_url' ]; then - enable_add_url "$j" 'allow' - else - enable_add_url "$j" 'block' +if [ -n "$pristine" ]; then + # shellcheck disable=SC1091 + . /lib/functions.sh + add_name() { + local cfg="$1" url name label + config_get url "$cfg" 'url' + config_get name "$cfg" 'name' + if [ -z "$name" ]; then + label="${url##*//}"; label="${label%%/*}" + name="$(_find_name "$url")" + if [ -n "$name" ]; then + uci set "${pkg}.${cfg}.name=${name}" + printf " %s: %s\n" "$label" "$name" >&2 fi - done - output_okn - done - uci_commit "$packageName" - output "Migrating simple-adblock cache file(s) " - for i in '/var/run/simple-adblock/dnsmasq.addnhosts.cache' \ - '/var/run/simple-adblock/dnsmasq.conf.cache' \ - '/var/run/simple-adblock/dnsmasq.ipset.cache' \ - '/var/run/simple-adblock/dnsmasq.nftset.cache' \ - '/var/run/simple-adblock/dnsmasq.servers.cache' \ - '/var/run/simple-adblock/unbound.cache'; do - if [ -s "$i" ]; then - current_dir="$(dirname "$i")" - mkdir -p "${current_dir//simple-adblock/adblock-fast}" - mv -f "$i" "${i//simple-adblock/adblock-fast}" && output_okn || output_failn fi - done - for i in 'simple-adblock.dnsmasq.addnhosts.gz' \ - 'simple-adblock.dnsmasq.conf.gz' \ - 'simple-adblock.dnsmasq.ipset.gz' \ - 'simple-adblock.dnsmasq.nftset.gz' \ - 'simple-adblock.dnsmasq.servers.gz' \ - 'simple-adblock.unbound.gz'; do - i="${ccd}/${i}" - if [ -s "$i" ]; then - mkdir -p "${ccd//simple-adblock/adblock-fast}" - mv -f "$i" "${i//simple-adblock/adblock-fast}" && output_okn || output_failn - fi - done - output_okn -fi - -# Transition to list names -_find_name() { grep -B1 "$1" "/etc/config/${packageName}-opkg" | head -1 | cut -d "'" -f2; } - -add_name() { - local cfg="$1" - local url name label - config_get url "$cfg" 'url' - config_get name "$cfg" 'name' - if [ -z "$name" ]; then - label="${url##*//}" - label="${label%%/*}"; - output "Finding name for ${label}: " - name="$(_find_name "$url")" - if [ -n "$name" ]; then - uci_set "$packageName" "$cfg" 'name' "$name" - output "$name " - output_okn - else - output "Unknown " - output_failn - fi - else - output "Name for ${label} already set to ${name} " - output_okn - fi -} - -if [ -s "/etc/config/${packageName}-opkg" ] && ! grep -q 'option name' "/etc/config/${packageName}"; then - config_load "$packageName" + } + config_load "$pkg" config_foreach add_name 'file_url' fi -# migrate to 1.2.0 -oldval="$(uci_get "$packageName" 'config' 'debug')" +# ── Migrate to 1.2.0 ──────────────────────────────────────────────── + +oldval="$(uci -q get "${pkg}.config.debug")" if [ -n "$oldval" ]; then - uci_set "$packageName" 'config' 'debug_init_script' "$oldval" - uci_remove "$packageName" 'config' 'debug' -fi -oldval="$(uci_get "$packageName" 'config' 'proc_debug')" -if [ -n "$oldval" ]; then - uci_set "$packageName" 'config' 'debug_performance' "$oldval" - uci_remove "$packageName" 'config' 'proc_debug' + uci set "${pkg}.config.debug_init_script=${oldval}" + uci -q delete "${pkg}.config.debug" fi -# migrate sanity_check to dnsmasq_sanity_check -if [ -z "$(uci_get "$packageName" 'config' 'dnsmasq_sanity_check')" ] && [ -n "$(uci_get "$packageName" 'config' 'sanity_check')" ]; then - oldval="$(uci_get "$packageName" 'config' 'sanity_check')" - uci_set "$packageName" 'config' 'dnsmasq_sanity_check' "$oldval" - uci_remove "$packageName" 'config' 'sanity_check' +oldval="$(uci -q get "${pkg}.config.proc_debug")" +if [ -n "$oldval" ]; then + uci set "${pkg}.config.debug_performance=${oldval}" + uci -q delete "${pkg}.config.proc_debug" fi -uci_changes "$packageName" && uci_commit "$packageName" +# ── Migrate sanity_check → dnsmasq_sanity_check ───────────────────── + +if [ -z "$(uci -q get "${pkg}.config.dnsmasq_sanity_check")" ] \ + && [ -n "$(uci -q get "${pkg}.config.sanity_check")" ]; then + oldval="$(uci -q get "${pkg}.config.sanity_check")" + uci set "${pkg}.config.dnsmasq_sanity_check=${oldval}" + uci -q delete "${pkg}.config.sanity_check" +fi + +# ── Commit if anything changed ─────────────────────────────────────── + +[ -n "$(uci -q changes "$pkg" 2>/dev/null)" ] && uci commit "$pkg" exit 0 diff --git a/net/adblock-fast/files/lib/adblock-fast/adblock-fast.uc b/net/adblock-fast/files/lib/adblock-fast/adblock-fast.uc new file mode 100644 index 0000000000..2967c0ec6f --- /dev/null +++ b/net/adblock-fast/files/lib/adblock-fast/adblock-fast.uc @@ -0,0 +1,2849 @@ +'use strict'; +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca). +// +// Main ucode module for adblock-fast. +// All business logic lives here; the init script is a thin procd wrapper. + +import { readfile, writefile, popen, stat, unlink, rename, open, glob, mkdir, mkstemp, symlink, chmod, chown, realpath, lsdir, access, dirname } from 'fs'; +import { cursor } from 'uci'; +import { connect } from 'ubus'; + +// ── Constants ─────────────────────────────────────────────────────── + +const pkg = { + name: 'adblock-fast', + version: 'dev-test', + compat: '13', + memory_threshold: 33554432, + config_file: '/etc/config/adblock-fast', + dnsmasq_file: '/var/run/adblock-fast/adblock-fast.dnsmasq', + run_file: '/dev/shm/adblock-fast', + triggers: { + reload: 'parallel_downloads debug download_timeout allowed_domain blocked_domain allowed_url blocked_url dns config_update_enabled config_update_url dnsmasq_config_file_url curl_additional_param curl_max_file_size curl_retry', + restart: 'compressed_cache compressed_cache_dir force_dns led force_dns_port', + }, +}; +pkg.service_name = pkg.name + ' ' + pkg.version; + +const dns_modes = { + 'dnsmasq.addnhosts': { + file: '/var/run/' + pkg.name + '/dnsmasq.addnhosts', + cache: '/var/run/' + pkg.name + '/dnsmasq.addnhosts.cache', + gzip: pkg.name + '.dnsmasq.addnhosts.gz', + format_filter: 's|^|127.0.0.1 |;s|$||', + format_filter_ipv6: 's|^|:: |;s|$||', + parse_filter: 's|^127.0.0.1 ||;s|^:: ||;', + grep_pattern_ipv4: 's|^|^127\\.0\\.0\\.1 |', + grep_pattern_ipv6: 's|^|^:: |', + }, + 'dnsmasq.conf': { + file: pkg.dnsmasq_file, + cache: '/var/run/' + pkg.name + '/dnsmasq.conf.cache', + gzip: pkg.name + '.dnsmasq.conf.gz', + format_filter: 's|^|local=/|;s|$|/|', + parse_filter: 's|local=/||;s|/$||;', + grep_pattern: 's|^|^local=/|;s|$|/$|', + }, + 'dnsmasq.ipset': { + file: pkg.dnsmasq_file, + cache: '/var/run/' + pkg.name + '/dnsmasq.ipset.cache', + gzip: pkg.name + '.dnsmasq.ipset.gz', + format_filter: 's|^|ipset=/|;s|$|/adb|', + parse_filter: 's|ipset=/||;s|/adb$||;', + grep_pattern: 's|^|^ipset=/|;s|$|/adb$|', + }, + 'dnsmasq.nftset': { + file: pkg.dnsmasq_file, + cache: '/var/run/' + pkg.name + '/dnsmasq.nftset.cache', + gzip: pkg.name + '.dnsmasq.nftset.gz', + format_filter: 's|^|nftset=/|;s|$|/4#inet#fw4#adb4|', + format_filter_ipv6: 's|^|nftset=/|;s|$|/4#inet#fw4#adb4,6#inet#fw4#adb6|', + parse_filter: 's|nftset=/||;s|/4#.*$||;', + grep_pattern: 's|^|^nftset=/|;s|$|/4#.*$|', + }, + 'dnsmasq.servers': { + file: '/var/run/' + pkg.name + '/dnsmasq.servers', + cache: '/var/run/' + pkg.name + '/dnsmasq.servers.cache', + gzip: pkg.name + '.dnsmasq.servers.gz', + format_filter: 's|^|server=/|;s|$|/|', + parse_filter: 's|server=/||;s|/.*$||;', + grep_pattern: 's|^|^server=/|;s|$|/$|', + allow_filter: 's|(.*)|server=/\\1/#|', + blocked_count_filter: '\\|/#|d', + }, + 'smartdns.domainset': { + file: '/var/run/' + pkg.name + '/smartdns.domainset', + cache: '/var/run/' + pkg.name + '/smartdns.domainset.cache', + gzip: pkg.name + '.smartdns.domainset.gz', + config: '/var/run/' + pkg.name + '/smartdns.domainset.conf', + format_filter: '', + parse_filter: '', + }, + 'smartdns.ipset': { + file: '/var/run/' + pkg.name + '/smartdns.ipset', + cache: '/var/run/' + pkg.name + '/smartdns.ipset.cache', + gzip: pkg.name + '.smartdns.ipset.gz', + config: '/var/run/' + pkg.name + '/smartdns.ipset.conf', + format_filter: '', + parse_filter: '', + }, + 'smartdns.nftset': { + file: '/var/run/' + pkg.name + '/smartdns.nftset', + cache: '/var/run/' + pkg.name + '/smartdns.nftset.cache', + gzip: pkg.name + '.smartdns.nftset.gz', + config: '/var/run/' + pkg.name + '/smartdns.nftset.conf', + format_filter: '', + parse_filter: '', + }, + 'unbound.adb_list': { + file: '/var/lib/unbound/adb_list.' + pkg.name, + cache: '/var/run/' + pkg.name + '/unbound.cache', + gzip: pkg.name + '.unbound.gz', + format_filter: 's|^|local-zone: "|;s|$|." always_nxdomain|', + parse_filter: 's|^local-zone: "||;s|." always_nxdomain$||;', + }, +}; + +const tmp = { + allowed: '/var/' + pkg.name + '.allowed.tmp', + a: '/var/' + pkg.name + '.a.tmp', + b: '/var/' + pkg.name + '.b.tmp', + sed: '/var/' + pkg.name + '.sed.tmp', +}; + +const list_formats = { + adblockplus: { + first_line: '[Adblock Plus]', + detect: "'^||'", + filter: "/^#/d;/^!/d;s/[[:space:]]*#.*$//;s/^||//;s/\\^$//;s/[[:space:]]*$//;s/[[:cntrl:]]$//;/[[:space:]]/d;/[`~!@#\\$%\\^&\\*()=+;:\"',<>?/\\|[{}]/d;/]/d;/\\./!d;/^$/d;/[^[:alnum:]_.-]/d;", + }, + dnsmasq: { + detect: "'^server='", + filter: "\\|^server=/[[:alnum:]_.-].*/|!d;s|server=/||;s|/.*$||", + }, + dnsmasq2: { + detect: "'^local='", + filter: "\\|^local=/[[:alnum:]_.-].*/|!d;s|local=/||;s|/.*$||", + }, + dnsmasq3: { + detect: "'^address='", + filter: "\\|^address=/[[:alnum:]_.-].*/|!d;s|address=/||;s|/.*$||", + }, + hosts: { + detect: "-e '^0\\.0\\.0\\.0\\s' -e '^127\\.0\\.0\\.1\\s'", + filter: "/localhost/d;/^#/d;/^[^0-9]/d;s/^0\\.0\\.0\\.0.//;s/^127\\.0\\.0\\.1.//;s/[[:space:]]*#.*$//;s/[[:cntrl:]]$//;s/[[:space:]]//g;/[`~!@#\\$%\\^&\\*()=+;:\"',<>?/\\|[{}]/d;/]/d;/\\./!d;/^$/d;/[^[:alnum:]_.-]/d;", + }, + domains: { + filter: "/^#/d;s/[[:space:]]*#.*//;s/[[:space:]]*$//;s/[[:cntrl:]]$//;/^[[:space:]]*$/d;/[[:space:]]/d;/^-/d;/^\\./d;/\\.\\./d;/-$/d;/\\.$/d;/^[0-9.]*$/d;/^[^[:alnum:]]/d;/[`~!@#\\$%\\^&\\*()=+;:\"',<>?/\\|{}]/d;/\\./!d", + }, +}; + +const sym = { + dot: ['.', '[w]'], + ok: ['\033[0;32m✓\033[0m', '\033[0;32m[✓]\033[0m'], + fail: ['\033[0;31m✗\033[0m', '\033[0;31m[✗]\033[0m'], + warn: ['\033[0;33m✔\033[0m', '\033[0;33m[✔]\033[0m'], + ERR: '\033[0;31m[ERROR]\033[0m', + WARN: '\033[0;33m[WARN]\033[0m', +}; + +const canary = { + mozilla: 'use-application-dns.net', + icloud: 'mask.icloud.com mask-h2.icloud.com', +}; + +// ── Mutable Module State ──────────────────────────────────────────── + +let state = { + script_name: pkg.name, + is_tty: false, + output_queue: '', + fw4_restart: false, +}; + +// ── Environment (platform capabilities, cached detection) ─────────── + +let env = { + // Platform capabilities (set by env.detect()) + dnsmasq_installed: false, + dnsmasq_features: '', + smartdns_installed: false, + unbound_installed: false, + ipset_supported: false, + nft_installed: false, + awk_cmd: 'awk', + + // Resolver service info (cached) + dnsmasq_ubus: null, + + // Downloader (set lazily by env.get_downloader()) + _dl_cache: null, + + // Guard flags + _detected: false, + _config_loaded: false, + _loaded: false, +}; + +let dns_output = { + allow_filter: '', + blocked_count_filter: '', + filter: '', + filter_ipv6: '', + file: '', + gzip: '', + cache: '', + config: '', + parse_filter: '', +}; + +// Config values loaded by env.load_config() +let cfg = {}; + +// ── Shell / System Helpers ────────────────────────────────────────── + +function shell_quote(s) { + return "'" + replace('' + s, "'", "'\\''") + "'"; +} + +function cmd_output(c) { + let p = popen(c, 'r'); + if (!p) return ''; + let data = p.read('all') || ''; + p.close(); + return trim(data); +} + +function cmd_rc(c) { + return system(c + ' >/dev/null 2>&1'); +} + +function ensure_trailing_newline(file) { + let fh = open(file, 'r+'); + if (!fh) return; + if (fh.seek(-1, 2) && fh.read(1) != '\n') + fh.write('\n'); + fh.close(); +} + +function mkdir_p(path) { + if (!path || stat(path)?.type == 'directory') return true; + let parent = dirname(path); + if (parent && parent != path) mkdir_p(parent); + return mkdir(path) != null; +} + +function is_present(cmd) { + if (index(cmd, '/') >= 0) + return access(cmd, 'x') == true; + for (let dir in ['/usr/sbin', '/usr/bin', '/sbin', '/bin']) + if (access(dir + '/' + cmd, 'x') == true) return true; + return false; +} + +function is_integer(v) { + if (v == null || v == '') return false; + if (!match('' + v, /^[0-9]+$/)) return false; + let n = int(v); + return n >= 1 && n <= 65535; +} + +function is_https_url(url) { + return substr('' + url, 0, 8) == 'https://'; +} + +function sanitize_domain(d) { + d = replace('' + d, /^[a-z]+:\/\//, ''); + d = replace(d, /\/.*$/, ''); + d = replace(d, /:.*$/, ''); + return d; +} + +function sanitize_dir(d) { + let r = realpath(d); + if (r && stat(r)?.type == 'directory') return r; + return null; +} + +function str_contains_word(haystack, needle) { + if (!haystack || !needle) return false; + return index(split('' + haystack, /\s+/), needle) >= 0; +} + +// ── Environment Detection ─────────────────────────────────────────── + +env.detect = function() { + if (env._detected) return; + env.dnsmasq_installed = is_present('dnsmasq'); + env.smartdns_installed = is_present('smartdns'); + env.unbound_installed = is_present('unbound'); + env.nft_installed = is_present('nft'); + env.ipset_supported = is_present('ipset') && cmd_rc('/usr/sbin/ipset help hash:net') == 0; + if (is_present('gawk')) env.awk_cmd = 'gawk'; + if (env.dnsmasq_installed && !env.dnsmasq_features) { + let raw = cmd_output('dnsmasq --version'); + let m = match(raw, /Compile time options:(.+)/); + env.dnsmasq_features = (m ? m[1] : '') + ' '; + } + env._detected = true; +}; + +env.get_downloader = function() { + if (env._dl_cache) return env._dl_cache; + let command, flag, ssl_supported; + if (is_present('curl')) { + command = 'curl -f --silent --insecure'; + if (cfg.curl_additional_param) command += ' ' + cfg.curl_additional_param; + if (cfg.curl_max_file_size) command += ' --max-filesize ' + cfg.curl_max_file_size; + if (cfg.curl_retry) command += ' --retry ' + cfg.curl_retry; + if (cfg.download_timeout) command += ' --connect-timeout ' + cfg.download_timeout; + flag = '-o'; + } else if (is_present('/usr/libexec/wget-ssl')) { + command = '/usr/libexec/wget-ssl --no-check-certificate -q'; + if (cfg.download_timeout) command += ' --timeout ' + cfg.download_timeout; + flag = '-O'; + } else if (is_present('wget') && cmd_rc("wget --version 2>/dev/null | grep -q '+https'") == 0) { + command = 'wget --no-check-certificate -q'; + if (cfg.download_timeout) command += ' --timeout ' + cfg.download_timeout; + flag = '-O'; + } else { + command = 'uclient-fetch --no-check-certificate -q'; + if (cfg.download_timeout) command += ' --timeout ' + cfg.download_timeout; + flag = '-O'; + } + ssl_supported = cmd_rc("curl --version 2>/dev/null | grep -q 'Protocols: .*https.*'") == 0 || + cmd_rc("wget --version 2>/dev/null | grep -q '+ssl'") == 0; + env._dl_cache = { command, flag, ssl_supported }; + return env._dl_cache; +}; + +// ── Shell Command Wrappers ────────────────────────────────────────── + +function sed_filter(expr, input, output) { + return system(sprintf('sed %s %s > %s', + shell_quote(expr), shell_quote(input), shell_quote(output))) == 0; +} + +function sed_inplace(expr, file) { + return system(sprintf('sed -i %s %s', + shell_quote(expr), shell_quote(file))) == 0; +} + +function sed_script(script, input, output) { + return system(sprintf('sed -E -f %s %s > %s', + shell_quote(script), shell_quote(input), shell_quote(output))) == 0; +} + +function sort_file(input, output, unique) { + return system(sprintf('sort %s%s > %s', + unique ? '-u ' : '', shell_quote(input), shell_quote(output))) == 0; +} + +function gzip_test(file) { + return system(sprintf('gzip -t -c %s >/dev/null 2>/dev/null', + shell_quote(file))) == 0; +} + +function gzip_compress(input, output) { + return system(sprintf('gzip < %s > %s', + shell_quote(input), shell_quote(output))) == 0; +} + +function gzip_decompress(input, output) { + return system(sprintf('gzip -dc < %s > %s', + shell_quote(input), shell_quote(output))) == 0; +} + +function grep_test(pattern, file, flags) { + return cmd_rc(sprintf('grep %s %s %s', + flags || '-q', shell_quote(pattern), shell_quote(file))) == 0; +} + +function grep_count(pattern, file, flags) { + return int(trim(cmd_output(sprintf('grep %s %s %s', + flags || '-c', shell_quote(pattern), shell_quote(file)))) || '0'); +} + +function grep_output(pattern, file, flags) { + return cmd_output(sprintf('grep %s %s %s', + flags || '', shell_quote(pattern), shell_quote(file))); +} + +function grep_exclude_file(patfile, input, output) { + return system(sprintf('grep -vFf %s %s > %s 2>/dev/null', + shell_quote(patfile), shell_quote(input), shell_quote(output))) == 0; +} + +function count_lines(file, filter_expr) { + if (filter_expr) + return int(trim(cmd_output(sprintf('sed %s %s | wc -l', + shell_quote(filter_expr), shell_quote(file)))) || '0'); + return int(trim(cmd_output('wc -l < ' + shell_quote(file))) || '0'); +} + +function awk_reverse_labels(input, output) { + return system(sprintf("%s -F '.' '{for(i=NF;i>0;i--) printf \"%%s%%s\", $i, (i>1?\".\":\"\\n\")}' %s > %s", + env.awk_cmd, shell_quote(input), shell_quote(output))) == 0; +} + +function awk_dedup_subdomains(input, output) { + return system(sprintf("%s 'NR==1{prev=$0;print;next}{len=length(prev);if(substr($0,1,len)==prev && substr($0,len+1,1)==\".\") next;print;prev=$0}' %s > %s", + env.awk_cmd, shell_quote(input), shell_quote(output))) == 0; +} + +function download(url, dest) { + let dlt = env.get_downloader(); + return system(sprintf('%s %s %s %s 2>/dev/null', + dlt.command, shell_quote(url), dlt.flag, shell_quote(dest))) == 0; +} + +function service_restart(name) { + return system(sprintf('/etc/init.d/%s restart >/dev/null 2>&1', name)) == 0; +} + +function service_enabled(name) { + return system(sprintf('/etc/init.d/%s enabled >/dev/null 2>&1', name)) == 0; +} + +// ── Memory / System Info ──────────────────────────────────────────── + +function get_mem_available() { + let conn = connect(); + if (!conn) return 0; + let info = conn.call('system', 'info'); + conn.disconnect(); + if (!info) return 0; + let ram = info?.memory?.available || 0; + let swap = info?.swap?.free || 0; + return ram + swap; +} + +function get_mem_total() { + let conn = connect(); + if (!conn) return 0; + let info = conn.call('system', 'info'); + conn.disconnect(); + if (!info) return 0; + let ram = info?.memory?.total || 0; + let swap = info?.swap?.total || 0; + return ram + swap; +} + +function led_on(l) { + if (l && stat(l + '/trigger')) + writefile(l + '/trigger', 'default-on\n'); +} + +function led_off(l) { + if (l && stat(l + '/trigger')) + writefile(l + '/trigger', 'none\n'); +} + +function logger(msg) { + system('/usr/bin/logger -t ' + shell_quote(state.script_name) + ' ' + shell_quote(msg)); +} + +function logger_debug(msg) { + if (cfg.debug_performance) + system('/usr/bin/logger -t ' + shell_quote(state.script_name) + ' ' + shell_quote(msg)); +} + +// ── Output Management ─────────────────────────────────────────────── + +let _write = function(level, ...args) { + if (!cfg.verbosity) + cfg.verbosity = int(uci(pkg.name).get(pkg.name, 'config', 'verbosity') || '1'); + let msg = join('', args); + if (level != null && (cfg.verbosity & level) == 0) return; + + // Print to stderr (terminal) + if (state.is_tty) + warn(replace(msg, /\\n/g, '\n')); + + // Queue for logger: flush on newline + if (index(msg, '\\n') >= 0 || index(msg, '\n') >= 0) { + msg = state.output_queue + msg; + state.output_queue = ''; + let clean = replace(msg, /\x1b\[[0-9;]*m/g, ''); + clean = replace(clean, /\\n/g, '\n'); + clean = trim(clean); + if (clean != '') + system('/usr/bin/logger -t ' + shell_quote(state.script_name) + ' ' + shell_quote(clean)); + } else { + state.output_queue += msg; + } +}; + +let output = { + _write: _write, + info: function(...args) { _write(1, ...args); }, + verbose: function(...args) { _write(2, ...args); }, + print: function(...args) { _write(null, ...args); }, + ok: function() { _write(1, sym.ok[0]); _write(2, sym.ok[1] + '\\n'); }, + okn: function() { _write(1, sym.ok[0] + '\\n'); _write(2, sym.ok[1] + '\\n'); }, + fail: function() { _write(1, sym.fail[0]); _write(2, sym.fail[1] + '\\n'); }, + failn: function() { _write(1, sym.fail[0] + '\\n'); _write(2, sym.fail[1] + '\\n'); }, + warn: function() { _write(1, sym.warn[0]); _write(2, sym.warn[1] + '\\n'); }, + warnn: function() { _write(1, sym.warn[0] + '\\n'); _write(2, sym.warn[1] + '\\n'); }, + dot: function() { _write(1, sym.dot[0]); _write(2, sym.dot[1]); }, + dns: function(msg) { + if (!cfg.dns) return; + let d = '' + cfg.dns; + if (index(d, 'dnsmasq.') == 0) _write(2, '[DNSM] ' + msg); + else if (index(d, 'smartdns.') == 0) _write(2, '[SMRT] ' + msg); + else if (index(d, 'unbound.') == 0) _write(2, '[UNBD] ' + msg); + }, + error: function(msg) { _write(null, sym.ERR + ' ' + msg + '!\\n'); }, + warning: function(msg) { _write(null, sym.WARN + ' ' + msg + '!\\n'); }, +}; + +// ── UCI Cursor ────────────────────────────────────────────────────── + +let _cursor = null; +let _cursor_loaded = {}; + +function uci(config, reload) { + if (!_cursor) _cursor = cursor(); + if (!_cursor_loaded[config] || reload) { + _cursor.load(config); + _cursor_loaded[config] = true; + } + return _cursor; +} + +function uci_has_changes(config) { + return length(uci(config).changes(config) || []) > 0; +} + +function uci_list_add_if_new(config, section, option, value) { + if (!config || !section || !option || !value) return false; + let ctx = uci(config); + let current = ctx.get(config, section, option); + if (type(current) == 'array' && index(current, value) >= 0) return true; + if (current == value) return true; + ctx.list_append(config, section, option, value); + ctx.save(config); + return true; +} + +// ── Status Data ───────────────────────────────────────────────────── + +let status_data = { + status: '', + message: '', + stats: '', + errors: [], + warnings: [], +}; + +function _load_status_from_ubus() { + let conn = connect(); + if (!conn) return; + let svc = conn.call('service', 'list', { name: pkg.name }); + conn.disconnect(); + let data = svc?.[pkg.name]?.data; + if (!data) return; + status_data.status = data.status || ''; + status_data.message = data.message || ''; + status_data.stats = data.stats || ''; + status_data.errors = data.errors || []; + status_data.warnings = data.warnings || []; +} + +function _update_ubus_status() { + let conn = connect(); + if (!conn) return; + let svc = conn.call('service', 'list', { name: pkg.name }); + let data = svc?.[pkg.name]?.data; + if (!data) { conn.disconnect(); return; } + data.status = status_data.status; + data.message = status_data.message; + data.stats = status_data.stats; + data.errors = []; + for (let e in status_data.errors) + push(data.errors, { code: e.code, info: e.info }); + data.warnings = []; + for (let e in status_data.warnings) + push(data.warnings, { code: e.code, info: e.info }); + conn.call('service', 'set_data', { name: pkg.name, data: data }); + conn.disconnect(); +} + +function _status_reset() { + status_data.status = ''; + status_data.message = ''; + status_data.stats = ''; + status_data.errors = []; + status_data.warnings = []; +} + +// ── get_text ──────────────────────────────────────────────────────── + +function get_text(r, ...args) { + let a = args[0] || ''; + switch (r) { + case 'errorConfigValidationFail': return sprintf("The %s config validation failed", pkg.name); + case 'errorServiceDisabled': return sprintf("The %s is currently disabled", pkg.name); + case 'errorNoDnsmasqIpset': return sprintf("The dnsmasq ipset support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support ipset", pkg.name); + case 'errorNoIpset': return sprintf("The dnsmasq ipset support is enabled in %s, but ipset is either not installed or installed ipset does not support 'hash:net' type", pkg.name); + case 'errorNoDnsmasqNftset': return sprintf("The dnsmasq nft set support is enabled in %s, but dnsmasq is either not installed or installed dnsmasq does not support nft set", pkg.name); + case 'errorNoNft': return sprintf("The dnsmasq nft sets support is enabled in %s, but nft is not installed", pkg.name); + case 'errorNoWanGateway': return sprintf("The %s failed to discover WAN gateway", pkg.service_name); + case 'errorOutputDirCreate': return sprintf("Failed to create directory for %s file", a); + case 'errorOutputFileCreate': return sprintf("Failed to create %s file", a); + case 'errorFailDNSReload': return "Failed to restart/reload DNS resolver"; + case 'errorSharedMemory': return "Failed to access shared memory"; + case 'errorSorting': return "Failed to sort data file"; + case 'errorOptimization': return "Failed to optimize data file"; + case 'errorAllowListProcessing': return "Failed to process allow-list"; + case 'errorDataFileFormatting': return "Failed to format data file"; + case 'errorCopyingDataFile': return sprintf("Failed to copy data file to '%s'", a); + case 'errorMovingDataFile': return sprintf("Failed to move data file to '%s'", a); + case 'errorCreatingCompressedCache': return "Failed to create compressed cache"; + case 'errorRemovingTempFiles': return "Failed to remove temporary files"; + case 'errorRestoreCompressedCache': return "Failed to unpack compressed cache"; + case 'errorRestoreCache': return sprintf("Failed to move '%s' to '%s'", dns_output.cache, dns_output.file); + case 'errorOhSnap': return "Failed to create block-list or restart DNS resolver"; + case 'errorStopping': return sprintf("Failed to stop %s", pkg.service_name); + case 'errorDNSReload': return "Failed to reload/restart DNS resolver"; + case 'errorDownloadingConfigUpdate': return "Failed to download Config Update file"; + case 'errorDownloadingList': return sprintf("Failed to download %s", a); + case 'errorParsingConfigUpdate': return "Failed to parse Config Update file"; + case 'errorParsingList': return "Failed to parse"; + case 'errorNoSSLSupport': return "No HTTPS/SSL support on device"; + case 'errorCreatingDirectory': return "Failed to create output/cache/gzip file directory"; + case 'errorDetectingFileType': return "Failed to detect format"; + case 'errorNothingToDo': return "No blocked list URLs nor blocked-domains enabled"; + case 'errorTooLittleRam': return sprintf("Free ram (%s) is not enough to process all enabled block-lists", a); + case 'errorCreatingBackupFile': return sprintf("Failed to create backup file %s", a); + case 'errorDeletingDataFile': return sprintf("Failed to delete data file %s", a); + case 'errorRestoringBackupFile': return sprintf("Failed to restore backup file %s", a); + case 'errorNoOutputFile': return sprintf("Failed to create final block-list %s", a); + case 'errorNoHeartbeat': return "Heartbeat domain is not accessible after resolver restart"; + case 'statusNoInstall': return sprintf("The %s is not installed or not found", pkg.service_name); + case 'statusStopped': return "stopped"; + case 'statusStarting': return "starting"; + case 'statusRestarting': return "restarting"; + case 'statusForceReloading': return "force-reloading"; + case 'statusDownloading': return "downloading"; + case 'statusProcessing': return "processing"; + case 'statusFail': return "failed to start"; + case 'statusSuccess': return "success"; + case 'statusTriggerBootWait': return "waiting for trigger (on_boot)"; + case 'statusTriggerStartWait': return "waiting for trigger (on_start)"; + case 'warningExternalDnsmasqConfig': return "Use of external dnsmasq config file detected, please set 'dns' option to 'dnsmasq.conf'"; + case 'warningMissingRecommendedPackages': return "Some recommended packages are missing"; + case 'warningInvalidCompressedCacheDir': return sprintf("Invalid compressed cache directory '%s'", a); + case 'warningFreeRamCheckFail': return "Can't detect free RAM"; + case 'warningSanityCheckTLD': return sprintf("Sanity check discovered TLDs in %s", a); + case 'warningSanityCheckLeadingDot': return sprintf("Sanity check discovered leading dots in %s", a); + case 'warningInvalidDomainsRemoved': return sprintf("Removed %s invalid domain entries from block-list (domains starting with -/./numbers or containing invalid patterns)", a); + default: return sprintf("Unknown error/warning '%s'", a); + } +} + +// ── Resolver Checks (env methods) ─────────────────────────────────── + +env.check_dnsmasq = function() { env.detect(); return env.dnsmasq_installed; }; +env.check_smartdns = function() { env.detect(); return env.smartdns_installed; }; +env.check_unbound = function() { env.detect(); return env.unbound_installed; }; +env.check_ipset = function() { env.detect(); return env.ipset_supported; }; +env.check_nft = function() { env.detect(); return env.nft_installed; }; + +env.check_dnsmasq_feature = function(feat) { + env.detect(); + switch (feat) { + case 'idn': return index(env.dnsmasq_features, ' IDN ') >= 0; + case 'ipset': return index(env.dnsmasq_features, ' ipset ') >= 0; + case 'nftset': return index(env.dnsmasq_features, ' nftset ') >= 0; + } + return false; +}; + +env.check_dnsmasq_ipset = function() { return env.check_ipset() && env.check_dnsmasq_feature('ipset'); }; +env.check_dnsmasq_nftset = function() { return env.check_nft() && env.check_dnsmasq_feature('nftset'); }; + +// ── Port/Firewall Helpers ─────────────────────────────────────────── + +function is_port_listening(port) { + if (!is_integer(port)) return false; + let port_hex = sprintf('%04X', int(port)); + for (let path in ['/proc/net/tcp', '/proc/net/tcp6']) { + let lines = split(readfile(path) || '', '\n'); + for (let i = 1; i < length(lines); i++) { + let fields = split(trim(lines[i]), /\s+/); + if (length(fields) < 4) continue; + if (uc(split(fields[1], ':')[1]) == port_hex && fields[3] == '0A') + return true; + } + } + for (let path in ['/proc/net/udp', '/proc/net/udp6']) { + let lines = split(readfile(path) || '', '\n'); + for (let i = 1; i < length(lines); i++) { + let fields = split(trim(lines[i]), /\s+/); + if (length(fields) < 2) continue; + if (uc(split(fields[1], ':')[1]) == port_hex) + return true; + } + } + return false; +} + +function is_fw4_restart_needed() { + if (state.fw4_restart) return true; + let d = (uci(pkg.name).get(pkg.name, 'config', 'dns') ?? 'dnsmasq.servers'); + let fd = (uci(pkg.name).get(pkg.name, 'config', 'force_dns') ?? '1'); + if (fd == '1') return true; + if (d == 'dnsmasq.ipset' || d == 'dnsmasq.nftset' || + d == 'smartdns.ipset' || d == 'smartdns.nftset') return true; + return false; +} + +// ── File Size Helpers ─────────────────────────────────────────────── + +function get_local_filesize(file) { + let s = stat(file); + return s ? s.size : null; +} + +function get_url_filesize(url) { // ucode-lsp disable + if (!url) return null; + let size = ''; + if (is_present('curl')) { + size = cmd_output(sprintf("curl --silent --insecure --fail --head --request GET --connect-timeout 2 %s | awk -F': ' '{IGNORECASE=1}/content-length/ {gsub(/\\r/, \"\"); print $2}'", shell_quote(url))); + } + if (!size && is_present('uclient-fetch')) { + size = cmd_output(sprintf("uclient-fetch --spider --timeout 2 %s -O /dev/null 2>&1 | sed -n '/^Download/ s/.*\\(\\([0-9]*\\) bytes\\).*/\\1/p'", shell_quote(url))); + } + return size ? size : null; +} + +// ── count_blocked_domains ─────────────────────────────────────────── + +function count_blocked_domains() { + if (!dns_output.file || !stat(dns_output.file)) return '0'; + if (dns_output.blocked_count_filter) + return '' + count_lines(dns_output.file, dns_output.blocked_count_filter); + return '' + count_lines(dns_output.file); +} + +// ── DNS Output Values ─────────────────────────────────────────────── + +env.dns_set_output_values = function(d) { + let dc = dns_modes[d]; + if (!dc) return; + dns_output.file = dc.file; + dns_output.cache = dc.cache; + dns_output.gzip = cfg.compressed_cache_dir + '/' + dc.gzip; + dns_output.parse_filter = dc.parse_filter; + dns_output.config = dc.config || ''; + dns_output.allow_filter = dc.allow_filter || ''; + dns_output.blocked_count_filter = dc.blocked_count_filter || ''; + dns_output.filter_ipv6 = ''; + if (d == 'dnsmasq.nftset' && cfg.ipv6_enabled && dc.format_filter_ipv6) + dns_output.filter = dc.format_filter_ipv6; + else + dns_output.filter = dc.format_filter; + if (d == 'dnsmasq.addnhosts' && cfg.ipv6_enabled && dc.format_filter_ipv6) + dns_output.filter_ipv6 = dc.format_filter_ipv6; +}; + +// ── adb_file ──────────────────────────────────────────────────────── + +function adb_file(action) { + switch (action) { + case 'create': + case 'backup': + if (stat(dns_output.file)?.size > 0) + return rename(dns_output.file, dns_output.cache) == true; + return false; + case 'restore': + case 'use': + if (stat(dns_output.cache)?.size > 0) + return rename(dns_output.cache, dns_output.file) == true; + return false; + case 'test': + case 'test_file': + return (stat(dns_output.file)?.size > 0); + case 'test_cache': + return (stat(dns_output.cache)?.size > 0); + case 'test_gzip': + return (stat(dns_output.gzip)?.size > 0) && gzip_test(dns_output.gzip); + case 'create_gzip': + if (!(stat(dns_output.file)?.size > 0)) return false; + unlink(dns_output.gzip); + // Write temp file in same directory as destination to avoid cross-filesystem rename + let gz_tmp = dns_output.gzip + '.tmp'; + if (gzip_compress(dns_output.file, gz_tmp)) { + if (rename(gz_tmp, dns_output.gzip)) { + return true; + } + unlink(gz_tmp); + } + return false; + case 'expand': + case 'unpack': + case 'unpack_gzip': + if (stat(dns_output.gzip)?.size > 0) + return gzip_decompress(dns_output.gzip, dns_output.cache); + return false; + case 'remove_cache': + unlink(dns_output.cache); + return true; + case 'remove_gzip': + unlink(dns_output.gzip); + return true; + } + return false; +} + +// ── Declarative Config Schema ─────────────────────────────────────── +// Each entry: [type, default] — mirrors the shell load_validate_config() spec. + +const config_schema = { // ucode-lsp disable + // Booleans + allow_non_ascii: ['bool', false], + canary_domains_icloud: ['bool', false], + canary_domains_mozilla: ['bool', false], + compressed_cache: ['bool', false], + config_update_enabled: ['bool', false], + debug_init_script: ['bool', false], + debug_performance: ['bool', false], + dnsmasq_sanity_check: ['bool', true], + dnsmasq_validity_check: ['bool', false], + enabled: ['bool', false], + force_dns: ['bool', true], + ipv6_enabled: ['bool', false], + parallel_downloads: ['bool', true], + procd_trigger_wan6: ['bool', false], + update_config_sizes: ['bool', true], + // Strings + config_update_url: ['string', 'https://cdn.jsdelivr.net/gh/openwrt/packages/net/adblock-fast/files/adblock-fast.config.update'], + curl_additional_param: ['string'], + curl_max_file_size: ['string'], + curl_retry: ['string', '3'], + dns: ['string', 'dnsmasq.servers'], + dnsmasq_config_file_url: ['string'], + download_timeout: ['string', '20'], + heartbeat_sleep_timeout: ['string', '10'], + led: ['string'], + pause_timeout: ['string', '20'], + procd_boot_wan_timeout: ['string', '60'], + // Integers + verbosity: ['int', 2], + // Lists + allowed_domain: ['list'], + blocked_domain: ['list'], + dnsmasq_instance: ['list', '*'], + force_dns_interface: ['list', 'lan'], + force_dns_port: ['list', '53 853'], + smartdns_instance: ['list', '*'], + // Domain (sanitized, '-' means disabled) + heartbeat_domain: ['domain', 'heartbeat.melmac.ca'], + // Directory (validated via realpath) + compressed_cache_dir: ['dir', '/etc'], +}; + +// ── parse_options ─────────────────────────────────────────────────── + +function parse_options(raw, schema) { // ucode-lsp disable + let result = {}; + for (let key in schema) { + let spec = schema[key]; + let v = raw[key]; + switch (spec[0]) { + case 'bool': + result[key] = (v == null) ? spec[1] : (v == '1' || v == 'yes' || v == 'on' || v == 'true'); + break; + case 'string': + result[key] = (v == null) ? (spec[1] ?? null) : '' + v; + break; + case 'int': + result[key] = (v == null) ? (spec[1] ?? 0) : int(v); + break; + case 'list': + if (v == null) { result[key] = spec[1] ?? null; } + else { result[key] = replace((type(v) == 'array') ? join(' ', v) : '' + v, /,/g, ' '); } + break; + case 'domain': + if (v == null || v == '-') result[key] = spec[1] ?? null; + else result[key] = sanitize_domain('' + v) || spec[1] || null; + break; + case 'dir': + let d = sanitize_dir('' + (v ?? spec[1] ?? '')); + result[key] = (d == '/') ? '' : (d || spec[1] || ''); + break; + } + } + return result; +} + +// ── env.load_config ───────────────────────────────────────────────── + +env.load_config = function() { + if (env._config_loaded) return; + state.is_tty = system('[ -t 2 ]') == 0 ? true : false; + let raw = uci(pkg.name, true).get_all(pkg.name, 'config') || {}; + cfg = parse_options(raw, config_schema); + env.dns_set_output_values(cfg.dns); + env._loaded = false; + env._detected = false; + env._dl_cache = null; + env._config_loaded = true; +}; + +// ── load_dl_command ───────────────────────────────────────────────── + +// Thin wrapper for backward compat; real logic is in env.get_downloader() +function load_dl_command() { env.get_downloader(); } + + +// ── detect_file_type ──────────────────────────────────────────────── + +function detect_file_type(file) { + let first_line = split(readfile(file) || '', '\n')[0]; + for (let name in keys(list_formats)) { + let fmt = list_formats[name]; + if (fmt.first_line && first_line == fmt.first_line) return name; + if (fmt.detect && cmd_rc("grep -q " + fmt.detect + " " + shell_quote(file)) == 0) return name; + } + if (list_formats.domains) { + let test = cmd_output(sprintf("sed %s %s 2>/dev/null | head -1", shell_quote(list_formats.domains.filter), shell_quote(file))); + if (test) return 'domains'; + } + return null; +} + +// ── adb_config_cache ──────────────────────────────────────────────── + +function adb_config_cache(action, variable) { + switch (action) { + case 'create': + case 'set': + writefile(pkg.run_file, readfile(pkg.config_file) || ''); + return; + case 'get': + switch (variable) { + case 'trigger_fw4': + if (stat(pkg.run_file)?.size > 0) { + if (is_fw4_restart_needed()) return 'true'; + } + return ''; + case 'trigger_service': + if (!(stat(pkg.run_file)?.size > 0)) return 'on_boot'; + if ((readfile(pkg.config_file) || '') != (readfile(pkg.run_file) || '')) { + // Config changed — determine if reload or restart + let run_dir = dirname(pkg.run_file); + let cached = cursor(run_dir); + cached.load(pkg.name); + let reload_triggers = split(pkg.triggers.reload, ' '); + for (let t in reload_triggers) { + if (!t) continue; + if (t == 'allowed_url' || t == 'blocked_url') continue; + let val_current = uci(pkg.name).get(pkg.name, 'config', t); + let val_old = cached.get(pkg.name, 'config', t); + if ('' + (val_current ?? '') != '' + (val_old ?? '')) return 'download'; + } + let restart_triggers = split(pkg.triggers.restart, ' '); + for (let t in restart_triggers) { + if (!t) continue; + let val_current = uci(pkg.name).get(pkg.name, 'config', t); + let val_old = cached.get(pkg.name, 'config', t); + if ('' + (val_current ?? '') != '' + (val_old ?? '')) return 'restart'; + } + } + return ''; + default: { + let run_dir = dirname(pkg.run_file); + let old_cfg = cursor(run_dir); + old_cfg.load(pkg.name); + return old_cfg.get(pkg.name, 'config', variable) ?? ''; + } + } + } +} + +// ── append_url (collect file_url sections) ────────────────────────── + +function append_urls() { // ucode-lsp disable + cfg.allowed_url = ''; + cfg.blocked_url = ''; + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => { + if (s.enabled == '0') return; + let url = s.url; + if (!url) return; + if ((s.action || 'block') == 'allow') + cfg.allowed_url = (cfg.allowed_url ? cfg.allowed_url + ' ' : '') + url; + else + cfg.blocked_url = (cfg.blocked_url ? cfg.blocked_url + ' ' : '') + url; + }); +} + +// ── env.load ──────────────────────────────────────────────────────── + +env.load = function(param, validation_result) { + if (env._loaded) return true; + env.load_config(); + + if (!cfg.enabled) { + push(status_data.errors, { code: 'errorServiceDisabled', info: '' }); + output.error(get_text('errorServiceDisabled')); + output.print("Run the following commands before starting service again:\\n"); + output.print("uci set " + pkg.name + ".config.enabled='1'; uci commit " + pkg.name + ";\\n"); + return false; + } + + if (validation_result && validation_result != '0') { + output.info(sym.fail[0] + '\\n'); + push(status_data.errors, { code: 'errorConfigValidationFail', info: '' }); + output.error(get_text('errorConfigValidationFail')); + output.print("Please check if the '" + pkg.config_file + "' contains correct values for config options.\\n"); + return false; + } + + // ── nested helpers ────────────────────────────────────────────── + + let _check_resolver_environment = function() { + // Check resolver presence + let dns_family = split(cfg.dns, '.')[0]; + switch (dns_family) { + case 'dnsmasq': + if (!env.check_dnsmasq()) { + if (param != 'quiet') { + push(status_data.errors, { code: 'errorDNSReload', info: '' }); + output.error("Resolver 'dnsmasq' not found"); + } + return false; + } + if (env.check_dnsmasq_feature('idn')) cfg.allow_non_ascii = false; + break; + case 'smartdns': + if (!env.check_smartdns()) { + if (param != 'quiet') { + push(status_data.errors, { code: 'errorDNSReload', info: '' }); + output.error("Resolver 'smartdns' not found"); + } + return false; + } + cfg.allow_non_ascii = false; + break; + case 'unbound': + if (!env.check_unbound()) { + if (param != 'quiet') { + push(status_data.errors, { code: 'errorDNSReload', info: '' }); + output.error("Resolver 'unbound' not found"); + } + return false; + } + cfg.allow_non_ascii = true; + break; + } + + // Check specific cfg.dns mode support + switch (cfg.dns) { + case 'dnsmasq.ipset': + if (!env.check_dnsmasq_feature('ipset')) { + if (param != 'quiet') push(status_data.errors, { code: 'errorNoDnsmasqIpset', info: '' }); + cfg.dns = 'dnsmasq.servers'; + } + if (!env.check_ipset()) { + if (param != 'quiet') push(status_data.errors, { code: 'errorNoIpset', info: '' }); + cfg.dns = 'dnsmasq.servers'; + } + break; + case 'dnsmasq.nftset': + if (!env.check_dnsmasq_feature('nftset')) { + if (param != 'quiet') push(status_data.errors, { code: 'errorNoDnsmasqNftset', info: '' }); + cfg.dns = 'dnsmasq.servers'; + } + if (!env.check_nft()) { + if (param != 'quiet') push(status_data.errors, { code: 'errorNoNft', info: '' }); + cfg.dns = 'dnsmasq.servers'; + } + break; + case 'smartdns.ipset': + if (!env.check_ipset()) { + if (param != 'quiet') push(status_data.errors, { code: 'errorNoIpset', info: '' }); + cfg.dns = 'smartdns.domainset'; + } + break; + case 'smartdns.nftset': + if (!env.check_nft()) { + if (param != 'quiet') push(status_data.errors, { code: 'errorNoNft', info: '' }); + cfg.dns = 'smartdns.domainset'; + } + break; + } + + if (cfg.dnsmasq_config_file_url) { + cfg.update_config_sizes = false; + if (cfg.dns != 'dnsmasq.conf') { + cfg.dns = 'dnsmasq.conf'; + if (param != 'quiet') + push(status_data.warnings, { code: 'warningExternalDnsmasqConfig', info: '' }); + } + } + + // Re-sync dns_output after any cfg.dns fallback + env.dns_set_output_values(cfg.dns); + + // Clean up files for non-active cfg.dns modes + for (let mode in dns_modes) { + if (mode == cfg.dns) continue; + let dc = dns_modes[mode]; + unlink(dc.cache); + unlink(cfg.compressed_cache_dir + '/' + dc.gzip); + if (dc.file != pkg.dnsmasq_file) unlink(dc.file); + if (dc.config) unlink(dc.config); + } + + return true; + }; + + let _setup_directories = function() { + let dirs = [pkg.run_file, dns_output.file, dns_output.cache, dns_output.gzip, dns_output.config]; + for (let f in dirs) { + if (!f) continue; + let dir = dirname(f); + if (!mkdir_p(dir)) { + if (param != 'quiet') + push(status_data.errors, { code: 'errorOutputDirCreate', info: f }); + } + } + }; + + let _check_recommended_packages = function() { + let bins = { + gawk: ['gawk', 'gawk'], + grep: ['/usr/libexec/grep-gnu', 'grep'], + sed: ['/usr/libexec/sed-gnu', 'sed'], + sort: ['/usr/libexec/sort-coreutils', 'coreutils-sort'], + }; + let missing = []; + for (let key in bins) { + if (!is_present(bins[key][0])) { + push(status_data.warnings, { code: 'warningMissingRecommendedPackages', info: bins[key][1] }); + push(missing, bins[key][1]); + } + } + if (length(missing) && param != 'quiet') { + output.warning(get_text('warningMissingRecommendedPackages') + ', install them by running:'); + output.print('opkg update; opkg --force-overwrite install ' + join(' ', missing) + ';'); + } + }; + + let _check_wan_gateway = function() { + let ub = connect(); + if (!ub) return false; + let dump = ub.call('network.interface', 'dump'); + ub.disconnect(); + if (!dump?.interface) return false; + for (let iface in dump.interface) { + for (let r in (iface.route || [])) + if (r.target == '0.0.0.0') return true; + } + return false; + }; + + // ── param-driven branches ─────────────────────────────────────── + + switch (param) { + case 'on_boot': + // Minimal: just config + dns_output (for cache restore) + break; + + case 'on_start': + case 'download': + case 'reload': + case 'restart': + default: + // Full pipeline + env.detect(); + if (!_check_resolver_environment()) return false; + _setup_directories(); + _check_recommended_packages(); + if (!_check_wan_gateway()) { + push(status_data.errors, { code: 'errorNoWanGateway', info: '' }); + output.error(get_text('errorNoWanGateway')); + return false; + } + append_urls(); + if (cfg.led) cfg.led = '/sys/class/leds/' + cfg.led; + break; + + case 'quiet': + env.detect(); + _check_resolver_environment(); + break; + + case 'rpcd': + env.detect(); + break; + } + + env._loaded = true; + return true; +}; + +// ── resolver ──────────────────────────────────────────────────────── + +function _dnsmasq_instance_get_confdir(inst) { + // Get the UCI section name for this instance + let uci_name = uci('dhcp').get('dhcp', inst, '.name') || inst; + // Cache dnsmasq service info via ubus + if (!env.dnsmasq_ubus) { + let ub = connect(); + if (ub) { + env.dnsmasq_ubus = ub.call('service', 'list', { name: 'dnsmasq' }); + ub.disconnect(); + } + } + // Extract the -C config file from the dnsmasq instance command line + let cfg_file = null; + let cmd_arr = env.dnsmasq_ubus?.dnsmasq?.instances?.[uci_name]?.command; + if (type(cmd_arr) == 'array') { + for (let i = 0; i < length(cmd_arr); i++) + if (cmd_arr[i] == '-C' && i + 1 < length(cmd_arr)) { cfg_file = cmd_arr[i + 1]; break; } + } + if (!cfg_file) return null; + // Parse conf-dir from the dnsmasq config file + let content = readfile(cfg_file) || ''; + if (!content) return null; + for (let line in split(content, '\n')) { + let m = match(line, /^conf-dir=(.+)$/); + if (m) return m[1]; + } + return null; +} + +function _dnsmasq_instance_config(inst, param) { + if (!stat('/etc/config/dhcp')?.size) return; + let dhcp = uci('dhcp'); + if (!dhcp.get('dhcp', inst)) return; + let confdir; + let addnhostsFile = dns_modes['dnsmasq.addnhosts'].file; + let confFile = dns_modes['dnsmasq.conf'].file; + let serversFile = dns_modes['dnsmasq.servers'].file; + switch (param) { + case 'dnsmasq.addnhosts': + confdir = _dnsmasq_instance_get_confdir(inst); + if (confdir) unlink(confdir + '/' + pkg.name); + dhcp.list_remove('dhcp', inst, 'addnmount', confFile); + if (dhcp.get('dhcp', inst, 'serversfile') == serversFile) + dhcp.delete('dhcp', inst, 'serversfile'); + uci_list_add_if_new('dhcp', inst, 'addnhosts', addnhostsFile); + break; + case 'cleanup': + case 'unbound.adb_list': + confdir = _dnsmasq_instance_get_confdir(inst); + if (confdir) unlink(confdir + '/' + pkg.name); + dhcp.list_remove('dhcp', inst, 'addnhosts', addnhostsFile); + dhcp.list_remove('dhcp', inst, 'addnmount', confFile); + if (dhcp.get('dhcp', inst, 'serversfile') == serversFile) + dhcp.delete('dhcp', inst, 'serversfile'); + break; + case 'dnsmasq.conf': + case 'dnsmasq.ipset': + case 'dnsmasq.nftset': + dhcp.list_remove('dhcp', inst, 'addnhosts', addnhostsFile); + if (dhcp.get('dhcp', inst, 'serversfile') == serversFile) + dhcp.delete('dhcp', inst, 'serversfile'); + uci_list_add_if_new('dhcp', inst, 'addnmount', confFile); + confdir = _dnsmasq_instance_get_confdir(inst); + if (!confdir) { dhcp.save('dhcp'); return; } + unlink(confdir + '/' + pkg.name); + symlink(confFile, confdir + '/' + pkg.name); + chmod(confdir + '/' + pkg.name, 0660); + chown(confdir + '/' + pkg.name, 'root', 'dnsmasq'); + break; + case 'dnsmasq.servers': + dhcp.list_remove('dhcp', inst, 'addnhosts', addnhostsFile); + confdir = _dnsmasq_instance_get_confdir(inst); + if (confdir) unlink(confdir + '/' + pkg.name); + dhcp.list_remove('dhcp', inst, 'addnmount', confFile); + if (dhcp.get('dhcp', inst, 'serversfile') != serversFile) + dhcp.set('dhcp', inst, 'serversfile', serversFile); + break; + } + dhcp.save('dhcp'); +} + +function _dnsmasq_instance_append_force_dns_port(inst) { + if (!stat('/etc/config/dhcp')?.size) return; + let dhcp = uci('dhcp'); + if (!dhcp.get('dhcp', inst)) return; + let instance_port = dhcp.get('dhcp', inst, 'port') ?? '53'; + if (!str_contains_word(cfg.force_dns_port, instance_port)) + cfg.force_dns_port = (cfg.force_dns_port ? cfg.force_dns_port + ' ' : '') + instance_port; +} + +function _smartdns_instance_config(inst, param) { + if (!stat('/etc/config/smartdns')?.size) return; + let sdns = uci('smartdns'); + if (!sdns.get('smartdns', inst)) return; + switch (param) { + case 'cleanup': + sdns.list_remove('smartdns', inst, 'conf_files', dns_output.config); + sdns.save('smartdns'); + unlink(dns_output.config); + break; + case 'smartdns.domainset': + writefile(dns_output.config, + 'domain-set -name adblock-fast -file ' + dns_output.file + '\n' + + 'domain-rules /domain-set:adblock-fast/ -a #\n'); + uci_list_add_if_new('smartdns', inst, 'conf_files', dns_output.config); + break; + case 'smartdns.ipset': + writefile(dns_output.config, + 'domain-set -name adblock-fast -file ' + dns_output.file + '\n' + + 'domain-rules /domain-set:adblock-fast/ -ipset adb\n'); + uci_list_add_if_new('smartdns', inst, 'conf_files', dns_output.config); + break; + case 'smartdns.nftset': + let nftset = '#4:inet#fw4#adb4'; + if (cfg.ipv6_enabled) nftset += ',#6:inet#fw4#adb6'; + writefile(dns_output.config, + 'domain-set -name adblock-fast -file ' + dns_output.file + '\n' + + 'domain-rules /domain-set:adblock-fast/ -nftset ' + nftset + '\n'); + uci_list_add_if_new('smartdns', inst, 'conf_files', dns_output.config); + break; + } +} + +function _smartdns_instance_append_force_dns_port(inst) { + if (!stat('/etc/config/smartdns')?.size) return; + let sdns = uci('smartdns'); + if (!sdns.get('smartdns', inst)) return; + let instance_port = sdns.get('smartdns', inst, 'port') ?? '53'; + if (!str_contains_word(cfg.force_dns_port, instance_port)) + cfg.force_dns_port = (cfg.force_dns_port ? cfg.force_dns_port + ' ' : '') + instance_port; +} + +function _unbound_instance_append_force_dns_port(inst) { + if (!stat('/etc/config/unbound')?.size) return; + let ubnd = uci('unbound'); + if (!ubnd.get('unbound', inst)) return; + let instance_port = ubnd.get('unbound', inst, 'listen_port') ?? '53'; + if (!str_contains_word(cfg.force_dns_port, instance_port)) + cfg.force_dns_port = (cfg.force_dns_port ? cfg.force_dns_port + ' ' : '') + instance_port; +} + +function _get_dnsmasq_instances() { + let result = []; + let dhcp_cur = cursor(); + dhcp_cur.load('dhcp'); + if (cfg.dnsmasq_instance == '*') { + dhcp_cur.foreach('dhcp', 'dnsmasq', (s) => push(result, s['.name'])); + } else if (cfg.dnsmasq_instance) { + for (let inst in split('' + cfg.dnsmasq_instance, /\s+/)) { + if (!inst) continue; + // Try @dnsmasq[N] index style, resolve to section name + let s = dhcp_cur.get_all('dhcp', '@dnsmasq[' + inst + ']'); + push(result, s?.['.name'] || inst); + } + } + return result; +} + +function _get_smartdns_instances() { + let result = []; + let sdns_cur = cursor(); + sdns_cur.load('smartdns'); + if (cfg.smartdns_instance == '*') { + sdns_cur.foreach('smartdns', 'smartdns', (s) => push(result, s['.name'])); + } else if (cfg.smartdns_instance) { + for (let inst in split('' + cfg.smartdns_instance, /\s+/)) { + if (!inst) continue; + let s = sdns_cur.get_all('smartdns', '@smartdns[' + inst + ']'); + push(result, s?.['.name'] || inst); + } + } + return result; +} + +function resolver(action) { + let resolver_name = split(cfg.dns, '.')[0]; + if (!action) return true; + + switch (action) { + case 'cleanup': + for (let mode in dns_modes) { + let dc = dns_modes[mode]; + unlink(dc.cache); + unlink(cfg.compressed_cache_dir + '/' + dc.gzip); + if (dc.file != pkg.dnsmasq_file) unlink(dc.file); + if (dc.config) unlink(dc.config); + } + if (stat('/etc/config/dhcp')?.size) { + for (let name in _get_dnsmasq_instances()) + _dnsmasq_instance_config(name, 'cleanup'); + if (uci_has_changes('dhcp')) uci('dhcp').commit('dhcp'); + } + if (stat('/etc/config/smartdns')?.size) { + for (let name in _get_smartdns_instances()) + _smartdns_instance_config(name, 'cleanup'); + if (uci_has_changes('smartdns')) uci('smartdns').commit('smartdns'); + } + break; + + case 'on_stop': + case 'quiet': + case 'quiet_restart': + return service_restart(resolver_name); + + case 'on_start': + if (!adb_file('test')) { + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorOutputFileCreate', info: dns_output.file }); + return false; + } + output.info('Cycling ' + resolver_name + ' '); + if (resolver('update_config') && resolver('test') && resolver('sanity') && resolver('restart') && resolver('heartbeat')) { + // success + } else { + resolver('revert'); + } + output.info('\\n'); + break; + + case 'test': + switch (cfg.dns) { + case 'dnsmasq.addnhosts': + case 'dnsmasq.conf': + case 'dnsmasq.ipset': + case 'dnsmasq.nftset': + case 'dnsmasq.servers': + output.dns('Testing ' + cfg.dns + ' configuration '); + if (cmd_rc('dnsmasq --test') == 0) { + output.ok(); + return true; + } + output.fail(); + return false; + default: + return true; + } + + case 'restart': + output.dns('Restarting ' + resolver_name + ' '); + status_data.message = 'Restarting ' + resolver_name; + if (service_restart(resolver_name)) { + status_data.status = 'statusSuccess'; + led_on(cfg.led); + output.ok(); + return true; + } + output.fail(); + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorDNSReload', info: '' }); + return false; + + case 'sanity': + if (!cfg.dnsmasq_sanity_check) return true; + output.dns('Sanity check for ' + cfg.dns + ' TLDs '); + if (!grep_test('\\.|server:', dns_output.file, '-qvE')) { + output.ok(); + } else { + push(status_data.warnings, { code: 'warningSanityCheckTLD', info: dns_output.file }); + output.warn(); + } + output.dns('Sanity check for ' + cfg.dns + ' leading dots '); + let dot_pattern; + switch (split(cfg.dns, '.')[0]) { + case 'dnsmasq': dot_pattern = '/\\.'; break; + case 'smartdns': dot_pattern = '^\\.'; break; + case 'unbound': dot_pattern = '"\\.'; break; + } + if (dot_pattern && !grep_test(dot_pattern, dns_output.file)) { + output.ok(); + } else { + push(status_data.warnings, { code: 'warningSanityCheckLeadingDot', info: dns_output.file }); + output.warn(); + } + return true; + + case 'heartbeat': + if (!cfg.heartbeat_domain) return true; + if (!is_integer(cfg.heartbeat_sleep_timeout)) return true; + output.dns('Probing ' + cfg.heartbeat_domain + ' for ' + cfg.heartbeat_sleep_timeout + ' seconds '); + status_data.message = 'Testing resolver on ' + cfg.heartbeat_domain; + let timeout = int(cfg.heartbeat_sleep_timeout); + for (let i = 0; i < timeout; i++) { + if (cmd_rc('resolveip ' + shell_quote(cfg.heartbeat_domain)) == 0) { + output.ok(); + return true; + } + output.dot(); + system('sleep 1'); + } + output.fail(); + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorNoHeartbeat', info: '' }); + return false; + + case 'revert': + output.info('Resetting/Restarting ' + resolver_name + ' '); + output.dns('Resetting ' + resolver_name + ' '); + resolver('cleanup'); + output.ok(); + output.dns('Restarting ' + resolver_name + ' '); + if (service_restart(resolver_name)) { + led_off(cfg.led); + output.ok(); + return true; + } + output.fail(); + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorDNSReload', info: '' }); + return false; + + case 'update_config': + output.dns('Updating ' + resolver_name + ' configuration '); + switch (split(cfg.dns, '.')[0]) { + case 'dnsmasq': + for (let name in _get_dnsmasq_instances()) { + _dnsmasq_instance_config(name, cfg.dns); + _dnsmasq_instance_append_force_dns_port(name); + } + if (uci_has_changes('dhcp')) uci('dhcp').commit('dhcp'); + if (adb_file('test')) { + chmod(dns_output.file, 0660); + chown(dns_output.file, 'root', 'dnsmasq'); + } else { + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorNoOutputFile', info: dns_output.file }); + return false; + } + break; + case 'smartdns': + for (let name in _get_smartdns_instances()) { + _smartdns_instance_config(name, cfg.dns); + _smartdns_instance_append_force_dns_port(name); + } + if (uci_has_changes('smartdns')) uci('smartdns').commit('smartdns'); + chmod(dns_output.file, 0660); + chmod(dns_output.config, 0660); + chown(dns_output.file, 'root', 'root'); + chown(dns_output.config, 'root', 'root'); + break; + case 'unbound': + let ubnd_cur = cursor(); + ubnd_cur.load('unbound'); + ubnd_cur.foreach('unbound', 'unbound', (s) => _unbound_instance_append_force_dns_port(s['.name'])); + chmod(dns_output.file, 0660); + chown(dns_output.file, 'root', 'unbound'); + break; + } + output.ok(); + return true; + } + return true; +} + +// ── process_file_url ──────────────────────────────────────────────── + +function process_file_url(section, url_override, action_override) { + let url, file_action, name, size_val; + + if (section && !url_override) { + let sec_cur = cursor(); + sec_cur.load(pkg.name); + let en = sec_cur.get(pkg.name, section, 'enabled'); + if (en == '0') return true; + url = sec_cur.get(pkg.name, section, 'url'); + file_action = sec_cur.get(pkg.name, section, 'action') || 'block'; + name = sec_cur.get(pkg.name, section, 'name'); + size_val = sec_cur.get(pkg.name, section, 'size'); + } else { + url = url_override; + file_action = action_override || 'block'; + } + + if (!cfg.enabled) return true; + if (!url) return false; + + let label = replace(url, /^[a-z]+:\/\//, ''); + label = replace(label, /\/.*$/, ''); + label = name || label; + label = 'List: ' + label; + + let type_name, d_tmp; + switch (file_action) { + case 'allow': type_name = 'Allowed'; d_tmp = tmp.allowed; break; + case 'block': type_name = 'Blocked'; d_tmp = tmp.b; break; + case 'file': type_name = 'File'; d_tmp = tmp.b; break; + } + + if (is_https_url(url) && !env.get_downloader().ssl_supported) { + output.info(sym.fail[0]); + output.verbose('[ DL ] ' + type_name + ' ' + label + ' ' + sym.fail[1] + '\\n'); + push(status_data.errors, { code: 'errorNoSSLSupport', info: name || url }); + return true; + } + + let r_tmp = trim(cmd_output('mktemp -q -t "' + pkg.name + '_tmp.XXXXXXXX"')); + if (!url || !download(url, r_tmp) || !(stat(r_tmp)?.size > 0)) { + output.info(sym.fail[0]); + output.verbose('[ DL ] ' + type_name + ' ' + label + ' ' + sym.fail[1] + '\\n'); + push(status_data.errors, { code: 'errorDownloadingList', info: name || url }); + } else { + // Ensure newline at end + ensure_trailing_newline(r_tmp); + + // Update size in config if changed + if (section) { + let new_size = get_local_filesize(r_tmp); + if (new_size != null && ('' + size_val) != ('' + new_size)) + uci(pkg.name).set(pkg.name, section, 'size', '' + new_size); + uci(pkg.name).save(pkg.name); + } + + let format = detect_file_type(r_tmp); + let filter = list_formats[format]?.filter; + if (!filter) { + output.info(sym.fail[0]); + output.verbose('[ DL ] ' + type_name + ' ' + label + ' ' + sym.fail[1] + '\\n'); + push(status_data.errors, { code: 'errorDetectingFileType', info: name || url }); + unlink(r_tmp); + return true; + } + if (format == 'hosts') + sed_inplace('/# Title: StevenBlack/,/# Custom host records are listed here/d', r_tmp); + + if (filter && file_action != 'file') + sed_inplace(filter, r_tmp); + + if (!(stat(r_tmp)?.size > 0)) { + output.info(sym.fail[0]); + output.verbose('[ DL ] ' + type_name + ' ' + label + ' (' + format + ') ' + sym.fail[1] + '\\n'); + push(status_data.errors, { code: 'errorParsingList', info: name || url }); + } else { + // Ensure file ends with newline, then append to accumulator + ensure_trailing_newline(r_tmp); + let inp = open(r_tmp, 'r'); + let out = open(d_tmp, 'a'); + if (inp && out) { + let chunk; + while ((chunk = inp.read(65536)) && length(chunk)) + out.write(chunk); + } + if (inp) inp.close(); + if (out) out.close(); + output.info(sym.ok[0]); + output.verbose('[ DL ] ' + type_name + ' ' + label + ' (' + format + ') ' + sym.ok[1] + '\\n'); + } + } + unlink(r_tmp); + return true; +} + +// ── download_dnsmasq_file ─────────────────────────────────────────── + +function download_dnsmasq_file() { + status_data.message = get_text('statusDownloading') + '...'; + status_data.status = 'statusDownloading'; + + for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed, dns_output.file, dns_output.cache]) unlink(f); + if (get_mem_available() < pkg.memory_threshold) { + output.print('Low free memory, restarting resolver '); + if (resolver('quiet_restart')) output.okn(); else output.failn(); + } + for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed]) writefile(f, ''); + output.info('Downloading dnsmasq file '); + process_file_url(null, cfg.dnsmasq_config_file_url, 'file'); + output.dns('Moving dnsmasq file '); + if (rename(tmp.b, dns_output.file)) { + output.ok(); + } else { + output.fail(); + push(status_data.errors, { code: 'errorMovingDataFile', info: dns_output.file }); + } + output.info('\\n'); +} + +// ── download_lists ────────────────────────────────────────────────── + +function download_lists() { + // RAM check + let free_mem = get_mem_available(); + if (!free_mem) { + push(status_data.warnings, { code: 'warningFreeRamCheckFail', info: '' }); + output.warning(get_text('warningFreeRamCheckFail')); + } else { + let total_sizes = 0; + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => { + if (s.enabled == '0') return; + let sz = s.size; + if (!sz && s.url) sz = get_url_filesize(s.url); + if (sz) total_sizes += int('' + sz); + }); + if (free_mem < total_sizes * 2) { + push(status_data.errors, { code: 'errorTooLittleRam', info: '' + free_mem }); + return false; + } + } + + status_data.message = get_text('statusDownloading') + '...'; + status_data.status = 'statusDownloading'; + + for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed, dns_output.file, dns_output.cache, dns_output.gzip]) unlink(f); + if (get_mem_total() < pkg.memory_threshold) { + output.print('Low free memory, restarting resolver '); + if (resolver('quiet_restart')) output.okn(); else output.failn(); + } + for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed]) writefile(f, ''); + + output.info('Downloading lists '); + + // Process each file_url section + let download_cfgs = []; + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => push(download_cfgs, s['.name'])); + + for (let cfg_name in download_cfgs) + process_file_url(cfg_name); + + if (uci_has_changes(pkg.name)) { + output.verbose('[PROC] Saving updated file sizes '); + if (cfg.update_config_sizes && uci(pkg.name).commit(pkg.name)) + output.ok(); + else + output.fail(); + } + output.info('\\n'); + + // Add canary domains and cfg.blocked_domain + let canaryDomains = ''; + if (cfg.canary_domains_icloud) + canaryDomains += (canaryDomains ? ' ' : '') + canary.icloud; + if (cfg.canary_domains_mozilla) + canaryDomains += (canaryDomains ? ' ' : '') + canary.mozilla; + + output.info('Processing downloads '); + + let start_time, end_time, elapsed, step_title; + + // Sort combined block-list + start_time = time(); + step_title = 'Sorting combined block-list'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.status = 'statusProcessing'; + status_data.message = get_text('statusProcessing') + ': ' + step_title; + + // Append cfg.blocked_domain and canary domains + ensure_trailing_newline(tmp.b); + let extra_domains = ''; + for (let hf in split((cfg.blocked_domain || '') + ' ' + canaryDomains, /\s+/)) { + if (hf) extra_domains += hf + '\n'; + } + if (extra_domains) { + let fd = popen(sprintf("sed %s >> %s", shell_quote(list_formats.domains.filter), shell_quote(tmp.b)), 'w'); + if (fd) { fd.write(extra_domains); fd.close(); } + } + sed_inplace('/^[[:space:]]*$/d', tmp.b); + + if (!(stat(tmp.b)?.size > 0)) return false; + + if (cfg.allow_non_ascii) { + if (sort_file(tmp.b, tmp.a, true)) + output.ok(); + else { output.fail(); push(status_data.errors, { code: 'errorSorting', info: '' }); } + } else { + if (system(sprintf("sort -u %s | grep -E -v '[^a-zA-Z0-9=/.-]' > %s", shell_quote(tmp.b), shell_quote(tmp.a))) == 0) + output.ok(); + else { output.fail(); push(status_data.errors, { code: 'errorSorting', info: '' }); } + } + end_time = time(); + elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + + // Optimization (subdomain dedup) + let needs_optimization = (cfg.dns == 'dnsmasq.conf' || cfg.dns == 'dnsmasq.ipset' || cfg.dns == 'dnsmasq.nftset' || + cfg.dns == 'dnsmasq.servers' || cfg.dns == 'smartdns.domainset' || cfg.dns == 'smartdns.ipset' || + cfg.dns == 'smartdns.nftset' || cfg.dns == 'unbound.adb_list'); + + if (needs_optimization) { + start_time = time(); + step_title = 'Optimizing combined block-list'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + + let ok = awk_reverse_labels(tmp.a, tmp.b); + if (ok) ok = sort_file(tmp.b, tmp.a); + if (ok) ok = awk_dedup_subdomains(tmp.a, tmp.b); + if (ok) ok = awk_reverse_labels(tmp.b, tmp.a); + if (ok) ok = sort_file(tmp.a, tmp.b, true); + if (ok) { output.ok(); } + else { + output.fail(); + push(status_data.errors, { code: 'errorOptimization', info: '' }); + rename(tmp.a, tmp.b); + } + end_time = time(); + elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + } else { + rename(tmp.a, tmp.b); + } + + // Remove allowed domains + if (cfg.allowed_domain || (stat(tmp.allowed)?.size > 0)) { + start_time = time(); + step_title = 'Removing allowed domains from combined block-list'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + + let allowed_extra = ''; + if (stat(tmp.allowed)?.size > 0) + allowed_extra = trim(cmd_output(sprintf("sed '/^[[:space:]]*$/d' %s", shell_quote(tmp.allowed)))); + let all_allowed = (cfg.allowed_domain || '') + (allowed_extra ? ' ' + allowed_extra : ''); + + let sed_content = ''; + for (let hf in split(all_allowed, /\s+/)) { + if (!hf) continue; + let escaped = replace(hf, /\./g, '\\.'); + sed_content += '/(^|\\.)' + escaped + '$/d;\n'; + } + if (sed_content) { + writefile(tmp.sed, sed_content); + if (sed_script(tmp.sed, tmp.b, tmp.a) && rename(tmp.a, tmp.b)) + output.ok(); + else { output.fail(); push(status_data.errors, { code: 'errorAllowListProcessing', info: '' }); } + } else { + output.fail(); + push(status_data.errors, { code: 'errorAllowListProcessing', info: '' }); + } + end_time = time(); + elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + } + + // Format combined block-list + start_time = time(); + step_title = 'Formatting combined block-list file'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + + if (!dns_output.filter_ipv6) { + if (dns_output.filter) { + if (sed_filter(dns_output.filter, tmp.b, tmp.a)) + output.ok(); + else { output.fail(); push(status_data.errors, { code: 'errorDataFileFormatting', info: '' }); } + } else { + writefile(tmp.a, readfile(tmp.b) || ''); + output.ok(); + } + } else { + if (cfg.dns == 'dnsmasq.addnhosts') { + if (sed_filter(dns_output.filter, tmp.b, tmp.a) && + system(sprintf('sed %s %s >> %s', shell_quote(dns_output.filter_ipv6), shell_quote(tmp.b), shell_quote(tmp.a))) == 0) + output.ok(); + else { output.fail(); push(status_data.errors, { code: 'errorDataFileFormatting', info: '' }); } + } + } + end_time = time(); + elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + + // Explicitly allow domains in servers mode + if (dns_output.allow_filter && cfg.allowed_domain) { + unlink(tmp.sed); writefile(tmp.sed, ''); + start_time = time(); + step_title = 'Explicitly allowing domains in ' + cfg.dns; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + let allow_input = ''; + for (let hf in split('' + cfg.allowed_domain, /\s+/)) + if (hf) allow_input += hf + '\n'; + if (allow_input) + system(sprintf("printf '%%s' %s | sed -E '%s' >> %s", shell_quote(allow_input), dns_output.allow_filter, shell_quote(tmp.sed))); + if (stat(tmp.sed)?.size > 0) { + if (writefile(tmp.b, (readfile(tmp.sed) || '') + (readfile(tmp.a) || ''))) + output.ok(); + else { output.fail(); push(status_data.errors, { code: 'errorAllowListProcessing', info: '' }); } + } else { + output.fail(); + push(status_data.errors, { code: 'errorAllowListProcessing', info: '' }); + } + end_time = time(); + elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + } else { + rename(tmp.a, tmp.b); + } + + // Move to output file + start_time = time(); + step_title = 'Setting up ' + cfg.dns + ' file'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + + if (rename(tmp.b, dns_output.file)) { + output.ok(); + } else { + output.fail(); + push(status_data.errors, { code: 'errorMovingDataFile', info: dns_output.file }); + } + if (cfg.dns == 'unbound.adb_list') + sed_inplace('1 i\\server:', dns_output.file); + + // Validity check + if (cfg.dnsmasq_validity_check && index(cfg.dns, 'dnsmasq.') == 0) { + start_time = time(); + step_title = 'Validating domain entries'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + let invalid_file = '/tmp/' + pkg.name + '.invalid.tmp'; + unlink(invalid_file); + system(sprintf("sed '%s' %s | grep -E '^-|^\\.|^[0-9.]+$|\\.\\.|\\-$|\\.$' > %s 2>/dev/null || true", + dns_output.parse_filter, shell_quote(dns_output.file), shell_quote(invalid_file))); + let invalid_count = 0; + if (stat(invalid_file)?.size > 0) { + invalid_count = int(trim(cmd_output('wc -l < ' + shell_quote(invalid_file))) || '0'); + if (invalid_count > 0) { + let dc = dns_modes[cfg.dns]; + let grep_pat = dc ? dc.grep_pattern : null; + if (cfg.dns == 'dnsmasq.addnhosts' && dc) { + system(sprintf("{ sed '%s' %s; sed '%s' %s; } > %s.pat 2>/dev/null", + dc.grep_pattern_ipv4, shell_quote(invalid_file), + dc.grep_pattern_ipv6, shell_quote(invalid_file), + shell_quote(invalid_file))); + grep_pat = null; + } + if (grep_pat) + sed_filter(grep_pat, invalid_file, invalid_file + '.pat'); + grep_exclude_file(invalid_file + '.pat', dns_output.file, dns_output.file + '.valid'); + rename(dns_output.file + '.valid', dns_output.file); + logger(sprintf('Removed %d invalid entries from %s.', invalid_count, cfg.dns)); + push(status_data.warnings, { code: 'warningInvalidDomainsRemoved', info: '' + invalid_count }); + unlink(invalid_file + '.pat'); + } + unlink(invalid_file); + } + if (invalid_count > 0) output.warn(); else output.ok(); + end_time = time(); + elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + } + + // Remove temporary files + step_title = 'Removing temporary files'; + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + for (let f in glob('/tmp/' + pkg.name + '_tmp.*') || []) unlink(f); + for (let f in [tmp.allowed, tmp.a, tmp.b, tmp.sed, dns_output.cache]) unlink(f); + output.ok(); + output.info('\\n'); + return true; +} + +// ── adb_config_update ─────────────────────────────────────────────── + +function adb_config_update(param) { + param = param || 'quiet'; + env.load_config(); + let label = replace('' + cfg.config_update_url, /^[a-z]+:\/\//, ''); + label = replace(label, /\/.*$/, ''); + if (!cfg.enabled) return; + if (!cfg.config_update_enabled) return; + + if (param != 'download') { + if (adb_file('test')) return; + if (adb_file('test_cache')) return; + if (adb_file('test_gzip')) return; + } + + output.info('Updating config '); + output.verbose('[ DL ] Config Update: ' + label + ' '); + let r_tmp = trim(cmd_output('mktemp -q -t "' + pkg.name + '_tmp.XXXXXXXX"')); + if (!download(cfg.config_update_url, r_tmp) || !(stat(r_tmp)?.size > 0)) { + output.failn(); + push(status_data.errors, { code: 'errorDownloadingConfigUpdate', info: '' }); + } else { + if (system(sprintf("sed -f %s -i %s 2>/dev/null", shell_quote(r_tmp), shell_quote(pkg.config_file))) == 0) + output.okn(); + else { output.failn(); push(status_data.errors, { code: 'errorParsingConfigUpdate', info: '' }); } + } + unlink(r_tmp); + // Cleanup missing URLs (refresh cursor after sed modified config) + let to_delete = []; + uci(pkg.name, true).foreach(pkg.name, 'file_url', (s) => { + if (!s.url) push(to_delete, s['.name']); + }); + for (let name in to_delete) + uci(pkg.name).delete(pkg.name, name); + uci(pkg.name).save(pkg.name); + if (uci_has_changes(pkg.name)) + uci(pkg.name).commit(pkg.name); +} + +// ── get_file_url_list ──────────────────────────────────────────────── + +function get_file_url_list() { + let files = []; + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => { + let size = s.size; + if (!size && s.url) size = get_url_filesize(s.url); + push(files, { name: s.name || s.url, url: s.url, size: size || '' }); + }); + return files; +} + +// ── _build_procd_data ─────────────────────────────────────────────── + +function _build_procd_data() { + let result = {}; + result.version = pkg.version; + result.status = status_data.status; + result.message = status_data.message; + result.stats = status_data.stats; + result.packageCompat = int(pkg.compat); + result.entries = int(count_blocked_domains()); + result.dns = cfg.dns; + result.outputFile = dns_output.file; + result.outputCache = dns_output.cache; + + let gzip_path = cfg.compressed_cache_dir + ? cfg.compressed_cache_dir + '/' + dns_output.gzip + : ''; + result.outputGzip = gzip_path; + + // Force DNS + let force_dns_ports = []; + if (cfg.force_dns && cfg.force_dns_port) { + force_dns_ports = split('' + cfg.force_dns_port, /[\s,]+/); + } + result.force_dns_active = length(force_dns_ports) > 0; + result.force_dns_ports = force_dns_ports; + + // Platform support + result.platform = { + ipset_installed: env.ipset_supported, + nft_installed: env.nft_installed, + dnsmasq_installed: env.dnsmasq_installed, + dnsmasq_ipset_support: env.check_dnsmasq_ipset(), + dnsmasq_nftset_support: env.check_dnsmasq_nftset(), + smartdns_installed: env.smartdns_installed, + smartdns_ipset_support: env.smartdns_installed && env.ipset_supported, + smartdns_nftset_support: env.smartdns_installed && env.nft_installed, + unbound_installed: env.unbound_installed, + leds: lsdir('/sys/class/leds') || [], + }; + + // File URL sizes + result.file_url = get_file_url_list(); + + // Errors + result.errors = []; + for (let e in status_data.errors) + push(result.errors, { code: e.code, info: e.info }); + + // Warnings + result.warnings = []; + for (let e in status_data.warnings) + push(result.warnings, { code: e.code, info: e.info }); + + // Firewall rules + result.firewall = []; + if (cfg.force_dns) { + let ports = split(replace('' + cfg.force_dns_port, /,/g, ' '), /\s+/); + for (let p in ports) { + if (!p) continue; + let ifaces = split('' + cfg.force_dns_interface, /\s+/); + if (is_port_listening(p)) { + for (let iface in ifaces) { + if (!iface) continue; + push(result.firewall, { + type: 'redirect', target: 'DNAT', src: iface, + proto: 'tcp udp', src_dport: '53', dest_port: '' + p, + family: 'any', reflection: false, + }); + } + } else { + for (let iface in ifaces) { + if (!iface) continue; + push(result.firewall, { + type: 'rule', src: iface, dest: '*', + proto: 'tcp udp', dest_port: '' + p, target: 'REJECT', + }); + } + } + } + } + + // fw4 restart flag (consumed by init script as shell variable) + result.fw4_restart_needed = is_fw4_restart_needed(); + + // ipset/nftset firewall rules + switch (cfg.dns) { + case 'dnsmasq.ipset': + case 'smartdns.ipset': + push(result.firewall, { type: 'ipset', name: 'adb', match: 'dest_net', storage: 'hash' }); + for (let iface in split('' + (cfg.force_dns_interface), /\s+/)) { + if (!iface) continue; + push(result.firewall, { type: 'rule', ipset: 'adb', src: iface, dest: '*', proto: 'tcp udp', target: 'REJECT' }); + } + break; + case 'dnsmasq.nftset': + case 'smartdns.nftset': + push(result.firewall, { type: 'ipset', name: 'adb4', family: '4', match: 'dest_net' }); + for (let iface in split('' + (cfg.force_dns_interface), /\s+/)) { + if (!iface) continue; + push(result.firewall, { type: 'rule', ipset: 'adb4', src: iface, dest: '*', proto: 'tcp udp', target: 'REJECT' }); + } + if (cfg.ipv6_enabled) { + push(result.firewall, { type: 'ipset', name: 'adb6', family: '6', match: 'dest_net' }); + for (let iface in split('' + (cfg.force_dns_interface), /\s+/)) { + if (!iface) continue; + push(result.firewall, { type: 'rule', ipset: 'adb6', src: iface, dest: '*', proto: 'tcp udp', target: 'REJECT' }); + } + } + break; + } + + return result; +} + +// ── emit_procd_shell ──────────────────────────────────────────────── +// Converts _build_procd_data() result into json_add_* shell commands +// for safe use between procd_open_data / procd_close_data. + +function emit_procd_shell(data) { + if (!data) return ''; + let lines = []; + + if (data.fw4_restart_needed) + push(lines, '_fw4_restart=1'); + + // Minimal data (e.g. from stop) — only emit shell variables + if (!data.version) + return join('\n', lines) + '\n'; + + push(lines, 'json_add_string version ' + shell_quote(data.version || '')); + push(lines, 'json_add_string status ' + shell_quote(data.status || '')); + push(lines, 'json_add_string message ' + shell_quote(data.message || '')); + push(lines, 'json_add_string stats ' + shell_quote(data.stats || '')); + push(lines, 'json_add_int packageCompat ' + shell_quote('' + (data.packageCompat || 0))); + push(lines, 'json_add_int entries ' + shell_quote('' + (data.entries || 0))); + push(lines, 'json_add_string dns ' + shell_quote(data.dns || '')); + push(lines, 'json_add_string outputFile ' + shell_quote(data.outputFile || '')); + push(lines, 'json_add_string outputCache ' + shell_quote(data.outputCache || '')); + push(lines, 'json_add_string outputGzip ' + shell_quote(data.outputGzip || '')); + push(lines, 'json_add_boolean force_dns_active ' + shell_quote(data.force_dns_active ? '1' : '0')); + + push(lines, 'json_add_array force_dns_ports'); + for (let p in (data.force_dns_ports || [])) + push(lines, 'json_add_string \'\' ' + shell_quote('' + p)); + push(lines, 'json_close_array'); + + // Platform support + push(lines, 'json_add_object platform'); + let plat = data.platform || {}; + push(lines, 'json_add_boolean ipset_installed ' + shell_quote(plat.ipset_installed ? '1' : '0')); + push(lines, 'json_add_boolean nft_installed ' + shell_quote(plat.nft_installed ? '1' : '0')); + push(lines, 'json_add_boolean dnsmasq_installed ' + shell_quote(plat.dnsmasq_installed ? '1' : '0')); + push(lines, 'json_add_boolean dnsmasq_ipset_support ' + shell_quote(plat.dnsmasq_ipset_support ? '1' : '0')); + push(lines, 'json_add_boolean dnsmasq_nftset_support ' + shell_quote(plat.dnsmasq_nftset_support ? '1' : '0')); + push(lines, 'json_add_boolean smartdns_installed ' + shell_quote(plat.smartdns_installed ? '1' : '0')); + push(lines, 'json_add_boolean smartdns_ipset_support ' + shell_quote(plat.smartdns_ipset_support ? '1' : '0')); + push(lines, 'json_add_boolean smartdns_nftset_support ' + shell_quote(plat.smartdns_nftset_support ? '1' : '0')); + push(lines, 'json_add_boolean unbound_installed ' + shell_quote(plat.unbound_installed ? '1' : '0')); + push(lines, 'json_add_array leds'); + for (let led in (plat.leds || [])) + push(lines, 'json_add_string \'\' ' + shell_quote('' + led)); + push(lines, 'json_close_array'); + push(lines, 'json_close_object'); + + // File URL sizes + push(lines, 'json_add_array file_url'); + for (let f in (data.file_url || [])) { + push(lines, "json_add_object ''"); + push(lines, 'json_add_string name ' + shell_quote(f.name || '')); + push(lines, 'json_add_string url ' + shell_quote(f.url || '')); + push(lines, 'json_add_string size ' + shell_quote('' + (f.size || ''))); + push(lines, 'json_close_object'); + } + push(lines, 'json_close_array'); + + push(lines, 'json_add_array errors'); + for (let e in (data.errors || [])) { + push(lines, "json_add_object ''"); + push(lines, 'json_add_string code ' + shell_quote(e.code || '')); + push(lines, 'json_add_string info ' + shell_quote(e.info || '')); + push(lines, 'json_close_object'); + } + push(lines, 'json_close_array'); + + push(lines, 'json_add_array warnings'); + for (let w in (data.warnings || [])) { + push(lines, "json_add_object ''"); + push(lines, 'json_add_string code ' + shell_quote(w.code || '')); + push(lines, 'json_add_string info ' + shell_quote(w.info || '')); + push(lines, 'json_close_object'); + } + push(lines, 'json_close_array'); + + push(lines, 'json_add_array firewall'); + for (let rule in (data.firewall || [])) { + push(lines, "json_add_object ''"); + for (let k in keys(rule)) { + let v = rule[k]; + if (type(v) == 'bool') + push(lines, 'json_add_boolean ' + k + ' ' + shell_quote(v ? '1' : '0')); + else if (type(v) == 'int') + push(lines, 'json_add_int ' + k + ' ' + shell_quote('' + v)); + else + push(lines, 'json_add_string ' + k + ' ' + shell_quote('' + v)); + } + push(lines, 'json_close_object'); + } + push(lines, 'json_close_array'); + + return join('\n', lines) + '\n'; +} + +// ── status_service ────────────────────────────────────────────────── + +function status_service(param) { + env.load_config(); + // When called from start() the in-memory status_data is already correct; + // reloading from ubus would overwrite it with stale data. + if (param != 'on_start_success' && param != 'on_start_failure') + _load_status_from_ubus(); + let status = status_data.status; + let message = status_data.message; + let stats = status_data.stats; + + if (status == 'statusSuccess') { + output.info('* ' + stats + '\\n'); + output.verbose('[STAT] ' + stats + '\\n'); + } else { + if (status) status = get_text(status); + if (status && message) status += ': ' + message; + let cache_info = ''; + let has_cache = adb_file('test_cache'); + let has_gzip = adb_file('test_gzip'); + if (has_cache && has_gzip) cache_info = 'cache file and compressed cache file found'; + else if (has_cache) cache_info = 'cache file found'; + else if (has_gzip) cache_info = 'compressed cache file found'; + if (status && cache_info) status += ' (' + cache_info + ')'; + if (status) output.print(pkg.service_name + ' ' + status + '.\\n'); + } + + if (param == 'quiet' || param == 'on_start_success' || param == 'on_start_failure') return; + + for (let e in status_data.errors) + output.error(get_text(e.code, e.info)); + for (let e in status_data.warnings) + output.warning(get_text(e.code, e.info)); +} + +// ── start ─────────────────────────────────────────────────────────── +// Returns JSON object for procd_open_data (status, firewall[], errors[], warnings[]) + +function start(args) { + let param = (args && args[0]) || 'on_start'; + + _load_status_from_ubus(); + let prev_status = status_data.status; + let prev_errors = length(status_data.errors) > 0; + _status_reset(); + + if (param == 'on_boot') { + env.load(param); // on_boot: just loads config + dns_output + if (!adb_file('test_gzip') && !adb_file('test_cache')) + return null; + } + + adb_config_update(param); + if (!env.load(param)) return null; // memoized if already called above + + let action = adb_config_cache('get', 'trigger_service'); + state.fw4_restart = adb_config_cache('get', 'trigger_fw4'); + + if (prev_errors) { + action = 'download'; + } else if (!adb_file('test')) { + if (adb_file('test_gzip') || adb_file('test_cache')) + action = 'restore'; + else + action = 'download'; + } else if (prev_status == 'statusSuccess') { + action = 'skip'; + } + + // Normalize action based on param + let combo = (action || '') + ':' + param; + if (index(combo, 'on_boot') >= 0 || param == 'on_pause') { + action = (adb_file('test_gzip') || adb_file('test_cache')) ? 'restore' : 'download'; + } else if (param == 'download' || action == 'download') { + action = 'download'; + } else if (action == 'restart') { + action = 'restart'; + } else if (action == 'restore') { + action = 'restore'; + } else if (action == 'skip') { + action = 'skip'; + } else if (!action) { + action = 'download'; + } + + if (action == 'restore') { + output.info('Starting ' + pkg.service_name + '...\\n'); + output.verbose('[INIT] Starting ' + pkg.service_name + '...\\n'); + status_data.status = 'statusStarting'; + if (adb_file('test_gzip') && !adb_file('test_cache') && !adb_file('test')) { + output.info('Found compressed cache file, unpacking it '); + output.verbose('[INIT] Found compressed cache file, unpacking it '); + status_data.message = 'found compressed cache file, unpacking it.'; + if (adb_file('unpack_gzip')) { + output.okn(); + } else { + output.failn(); + output.error(get_text('errorRestoreCompressedCache')); + action = 'download'; + } + } + if (adb_file('test_cache') && !adb_file('test')) { + output.info('Found cache file, reusing it '); + output.verbose('[INIT] Found cache file, reusing it '); + status_data.message = 'found cache file, reusing it.'; + if (adb_file('restore')) { + cfg.dnsmasq_sanity_check = false; + cfg.heartbeat_domain = null; + output.okn(); + resolver('on_start'); + } else { + output.failn(); + output.error(get_text('errorRestoreCache')); + action = 'download'; + } + } + } + + if (action == 'download') { + if (!cfg.blocked_url && !cfg.blocked_domain) { + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorNothingToDo', info: '' }); + } else { + if (!adb_file('test') || adb_file('test_cache') || adb_file('test_gzip')) { + output.info('Force-reloading ' + pkg.service_name + '...\\n'); + output.verbose('[INIT] Force-reloading ' + pkg.service_name + '...\\n'); + status_data.status = 'statusForceReloading'; + } else { + output.info('Starting ' + pkg.service_name + '...\\n'); + output.verbose('[INIT] Starting ' + pkg.service_name + '...\\n'); + status_data.status = 'statusStarting'; + } + resolver('cleanup'); + if (cfg.dns == 'dnsmasq.conf' && cfg.dnsmasq_config_file_url) + download_dnsmasq_file(); + else + download_lists(); + resolver('on_start'); + } + } + + if (action == 'restart') { + output.info('Restarting ' + pkg.service_name + '...\\n'); + output.verbose('[INIT] Restarting ' + pkg.service_name + '...\\n'); + status_data.status = 'statusRestarting'; + cfg.dnsmasq_sanity_check = false; + cfg.heartbeat_domain = null; + resolver('on_start'); + } + + if (action == 'start') { + output.info('Starting ' + pkg.service_name + '...\\n'); + output.verbose('[INIT] Starting ' + pkg.service_name + '...\\n'); + status_data.status = 'statusStarting'; + cfg.dnsmasq_sanity_check = false; + cfg.heartbeat_domain = null; + resolver('on_start'); + } + + let final_status = status_data.status; + if (adb_file('test') && final_status != 'statusFail') { + status_data.message = ''; + status_data.status = 'statusSuccess'; + status_data.stats = pkg.service_name + ' is blocking ' + count_blocked_domains() + ' domains (with ' + cfg.dns + ')'; + status_service('on_start_success'); + } else { + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorOhSnap', info: '' }); + status_service('on_start_failure'); + resolver('revert'); + } + + // Compressed cache: create or remove + if (cfg.compressed_cache && !adb_file('test_gzip') && adb_file('test')) { + let start_time = time(); + let step_title = 'Creating ' + cfg.dns + ' compressed cache'; + output.info(step_title + ' '); + output.verbose('[PROC] ' + step_title + ' '); + status_data.message = get_text('statusProcessing') + ': ' + step_title; + if (adb_file('create_gzip')) + output.okn(); + else { + output.failn(); + push(status_data.errors, { code: 'errorCreatingCompressedCache', info: '' }); + } + let end_time = time(); + let elapsed = end_time - start_time; + logger_debug('[PERF-DEBUG] ' + step_title + ' took ' + elapsed + 's'); + } else { + adb_file('remove_gzip'); + } + + adb_config_cache('create'); + + return _build_procd_data(); +} + +// ── dl ────────────────────────────────────────────────────────────── + +function dl() { + return start(['download']); +} + +// ── stop ──────────────────────────────────────────────────────────── + +function stop() { + env.load_config(); + if (adb_file('test')) { + output.info('Stopping ' + pkg.service_name + '... '); + output.verbose('[STOP] Stopping ' + pkg.service_name + '... '); + adb_file('create'); + if (resolver('on_stop')) { + system('ipset -q -! flush adb 2>/dev/null; ipset -q -! destroy adb 2>/dev/null'); + system('nft delete set inet fw4 adb4 2>/dev/null; nft delete set inet fw4 adb6 2>/dev/null'); + led_off(cfg.led); + output.okn(); + status_data.status = 'statusStopped'; + status_data.message = ''; + } else { + output.failn(); + status_data.status = 'statusFail'; + push(status_data.errors, { code: 'errorStopping', info: '' }); + output.error(get_text('errorStopping')); + } + } + return { fw4_restart_needed: is_fw4_restart_needed() }; +} + +// ── Extra Commands ────────────────────────────────────────────────── + +function allow(string) { + env.load_config(); + if (!adb_file('test')) { + output.print("No block-list ('" + dns_output.file + "') found.\\n"); + return; + } + if (!string) { + output.print("Usage: /etc/init.d/" + pkg.name + " allow 'domain' ...\\n"); + return; + } + if (cfg.dnsmasq_config_file_url) { + output.print("Allowing individual domains is not possible when using external dnsmasq config file.\\n"); + return; + } + + let resolver_name = split(cfg.dns, '.')[0]; + output.info('Allowing domains and restarting ' + resolver_name + ' '); + output.verbose('[PROC] Allowing domains \\n'); + + for (let c in split('' + string, /\s+/)) { + if (!c) continue; + output.verbose(' ' + c + ' '); + let escaped = replace(c, /\./g, '\\.'); + switch (split(cfg.dns, '.')[0]) { + case 'dnsmasq': + sed_inplace(sprintf('\\:/\\(/%s\\|.%s\\):d', escaped, escaped), dns_output.file); + break; + case 'smartdns': + case 'unbound': + sed_inplace(sprintf('\\:\\("%s\\|.%s"\\):d', escaped, escaped), dns_output.file); + break; + } + output.ok(); + if (dns_output.allow_filter) { + system(sprintf("echo %s | sed -E '%s' >> %s", shell_quote(c), dns_output.allow_filter, shell_quote(dns_output.file))); + output.ok(); + } + uci_list_add_if_new(pkg.name, 'config', 'allowed_domain', c); + output.ok(); + } + + if (cfg.compressed_cache) { + output.verbose('[PROC] Creating compressed cache '); + if (adb_file('create_gzip')) output.ok(); else output.fail(); + } + output.verbose('[PROC] Committing changes to config '); + if (uci(pkg.name).commit(pkg.name)) { + let ad = uci(pkg.name).get(pkg.name, 'config', 'allowed_domain'); + cfg.allowed_domain = ad ? replace((type(ad) == 'array') ? join(' ', ad) : '' + ad, /,/g, ' ') : null; + adb_config_cache('create'); + status_data.stats = pkg.service_name + ' is blocking ' + count_blocked_domains() + ' domains (with ' + cfg.dns + ')'; + output.ok(); + if (cfg.dns == 'dnsmasq.ipset') { + output.verbose('[PROC] Flushing adb ipset '); + if (system('ipset -q -! flush adb 2>/dev/null') == 0) output.ok(); else output.fail(); + } + if (cfg.dns == 'dnsmasq.nftset') { + output.verbose('[PROC] Flushing adb nft sets '); + system('nft flush set inet fw4 adb6 2>/dev/null'); + if (system('nft flush set inet fw4 adb4 2>/dev/null') == 0) output.ok(); else output.fail(); + } + output.dns('Restarting ' + resolver_name + ' '); + if (service_restart(resolver_name)) output.ok(); else output.fail(); + } else { + output.fail(); + } + _update_ubus_status(); + output.info('\\n'); +} + +function check(param) { + env.load_config(); + if (!adb_file('test')) { + output.print("No block-list ('" + dns_output.file + "') found.\\n"); + return; + } + if (!param) { + output.print("Usage: /etc/init.d/" + pkg.name + " check 'domain' ...\\n"); + return; + } + for (let string in split('' + param, /\s+/)) { + if (!string) continue; + let c = grep_count(string, dns_output.file, '-c -E'); + if (c > 0) { + let word = (c == 1) ? '1 match' : c + ' matches'; + output.info("Found " + word + " for '" + string + "' in '" + dns_output.file + "'.\\n"); + output.verbose("[PROC] Found " + word + " for '" + string + "' in '" + dns_output.file + "'.\\n"); + if (c <= 20) { + let matches = grep_output(string, dns_output.file); + if (dns_output.parse_filter) + matches = cmd_output(sprintf("grep %s %s | sed '%s'", shell_quote(string), shell_quote(dns_output.file), dns_output.parse_filter)); + if (matches) output.print(matches + '\\n'); + } + } else { + output.info("The '" + string + "' is not found in current block-list ('" + dns_output.file + "').\\n"); + output.verbose("[PROC] The '" + string + "' is not found in current block-list ('" + dns_output.file + "').\\n"); + } + } +} + +function check_tld() { + env.load_config(); + if (!adb_file('test')) { + output.print("No block-list ('" + dns_output.file + "') found.\\n"); + return; + } + let c = grep_count('\\.|server:', dns_output.file, '-cvE'); + if (c > 0) { + let word = (c == 1) ? '1 match for TLD' : c + ' matches for TLDs'; + output.info("Found " + word + " in '" + dns_output.file + "'.\\n"); + output.verbose("[PROC] Found " + word + " in '" + dns_output.file + "'.\\n"); + if (c <= 20) { + let matches = grep_output('\\.|server:', dns_output.file, '-vE'); + if (dns_output.parse_filter) + matches = cmd_output(sprintf("grep -vE '\\.|server:' %s | sed '%s'", shell_quote(dns_output.file), dns_output.parse_filter)); + if (matches) output.print(matches + '\\n'); + } + } else { + output.info("No TLD was found in current block-list ('" + dns_output.file + "').\\n"); + output.verbose("[PROC] No TLD was found in current block-list ('" + dns_output.file + "').\\n"); + } +} + +function check_leading_dot() { + env.load_config(); + if (!adb_file('test')) { + output.print("No block-list ('" + dns_output.file + "') found.\\n"); + return; + } + let search_string = ''; + switch (split(cfg.dns, '.')[0]) { + case 'dnsmasq': search_string = '/\\.'; break; + case 'smartdns': search_string = '^\\.'; break; + case 'unbound': search_string = '"\\.'; break; + default: return; + } + let c = grep_count(search_string, dns_output.file); + if (c > 0) { + let word = (c == 1) ? '1 match for leading-dot domain' : c + ' matches for leading-dot domains'; + output.info("Found " + word + " in '" + dns_output.file + "'.\\n"); + output.verbose("[PROC] Found " + word + " in '" + dns_output.file + "'.\\n"); + if (c <= 20) { + let matches = grep_output(search_string, dns_output.file); + if (dns_output.parse_filter) + matches = cmd_output(sprintf("grep %s %s | sed '%s'", shell_quote(search_string), shell_quote(dns_output.file), dns_output.parse_filter)); + if (matches) output.print(matches + '\\n'); + } + } else { + output.info("No leading-dot domain was found in current block-list ('" + dns_output.file + "').\\n"); + output.verbose("[PROC] No leading-dot domain was found in current block-list ('" + dns_output.file + "').\\n"); + } +} + +function check_lists(param) { + env.load_config(); + if (!param) { + output.print("Usage: /etc/init.d/" + pkg.name + " check_lists 'domain' ...\\n"); + return; + } + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => { + if (s.enabled == '0') return; + if ((s.action || 'block') != 'block') return; + let url = s.url; + let name = s.name || url; + if (!url) return; + + output.info('Checking ' + name + ': '); + output.verbose('[ DL ] ' + name + ' '); + + if (is_https_url(url) && !env.get_downloader().ssl_supported) { + output.failn(); + return; + } + let r_tmp = trim(cmd_output('mktemp -q -t "' + pkg.name + '_tmp.XXXXXXXX"')); + if (!download(url, r_tmp) || !(stat(r_tmp)?.size > 0)) { + output.failn(); + return; + } + output.verbose(sym.ok[1] + '\\n'); + ensure_trailing_newline(r_tmp); + + for (let string in split('' + param, /\s+/)) { + if (!string) continue; + let c = grep_count(string, r_tmp, '-c -E'); + if (c > 0) { + let word = (c == 1) ? '1 match' : c + ' matches'; + output.info("found " + word + " for '" + string + "'.\\n"); + output.verbose("[PROC] Found " + word + " for '" + string + "' in '" + url + "'.\\n"); + let matches = grep_output(string, r_tmp); + if (matches) output.print(matches + '\\n'); + } else { + output.info("'" + string + "' not found.\\n"); + output.verbose("[PROC] The '" + string + "' is not found in '" + url + "'.\\n"); + } + } + unlink(r_tmp); + }); +} + +function killcache() { + env.load_config(); + for (let mode in dns_modes) { + let dc = dns_modes[mode]; + unlink(dc.cache); + unlink(cfg.compressed_cache_dir + '/' + dc.gzip); + } + resolver('cleanup'); +} + +function pause(timeout) { + env.load_config(); + timeout = timeout || cfg.pause_timeout || '20'; + stop(); + output.info('Sleeping for ' + timeout + ' seconds... '); + output.verbose('[PROC] Sleeping for ' + timeout + ' seconds... '); + if (is_integer(timeout) && system('sleep ' + timeout) == 0) + output.okn(); + else + output.failn(); + let result = start(['on_pause']); + if (result) { + let conn = connect(); + if (conn) { + conn.call('service', 'set_data', { name: pkg.name, data: result }); + conn.disconnect(); + } + } +} + +function show_blocklist() { + env.load_config(); + if (dns_output.file && dns_output.parse_filter) + system(sprintf("sed '%s' %s", dns_output.parse_filter, shell_quote(dns_output.file))); + else if (dns_output.file) + print(readfile(dns_output.file) || ''); +} + +function sizes() { + env.load_config(); + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => { + let size = get_url_filesize(s.url); + output.print((s.name || s.url) + (size ? ': ' + size : '') + ' '); + if (size) { + uci(pkg.name).set(pkg.name, s['.name'], 'size', '' + size); + output.okn(); + } else { + output.failn(); + } + }); + uci(pkg.name).save(pkg.name); + if (cfg.update_config_sizes && length(uci(pkg.name).changes(pkg.name) || [])) + uci(pkg.name).commit(pkg.name); +} + +// ── get_network_trigger_info (for service_triggers) ───────────────── + +function get_network_trigger_info() { + env.load_config(); + let result = { procd_trigger_wan6: cfg.procd_trigger_wan6 }; + return result; +} + +// ── rpcd Data Functions ───────────────────────────────────────────── + +function get_init_status(name) { + name = name || pkg.name; + env.load('rpcd'); + + // Read pre-computed data from procd service (like PBR) + let conn = connect(); + let ubus_data = conn ? conn.call('service', 'list', { name: pkg.name }) : null; + if (conn) conn.disconnect(); + let svc_data = ubus_data?.[pkg.name]?.data; + + // Gzip path (for live file-existence checks) + let gzip_path = svc_data?.outputGzip || ''; + if (!gzip_path && cfg.compressed_cache_dir) + gzip_path = cfg.compressed_cache_dir + '/' + dns_output.gzip; + + let result = {}; + result[name] = { + version: pkg.version, + packageCompat: int(pkg.compat), + + // Live-computed (cheap stat/uci checks) + enabled: service_enabled(pkg.name), + running: (stat(pkg.run_file)?.size > 0), + outputFileExists: (stat(svc_data?.outputFile || dns_output.file)?.size > 0) || false, + outputCacheExists: (stat(svc_data?.outputCache || dns_output.cache)?.size > 0) || false, + outputGzipExists: gzip_path ? (stat(gzip_path)?.size > 0) || false : false, + + // From procd ubus data (pre-computed at start/dl time) + status: svc_data?.status || '', + message: svc_data?.message || '', + stats: svc_data?.stats || '', + entries: svc_data?.entries || 0, + dns: svc_data?.dns || cfg.dns, + outputFile: svc_data?.outputFile || dns_output.file, + outputCache: svc_data?.outputCache || dns_output.cache, + outputGzip: gzip_path, + force_dns_active: svc_data?.force_dns_active || false, + force_dns_ports: svc_data?.force_dns_ports || [], + errors: svc_data?.errors || [], + warnings: svc_data?.warnings || [], + + // Platform support (from env.detect, runs once per rpcd lifetime) + platform: { + ipset_installed: env.ipset_supported, + nft_installed: env.nft_installed, + dnsmasq_installed: env.dnsmasq_installed, + dnsmasq_ipset_support: env.check_dnsmasq_ipset(), + dnsmasq_nftset_support: env.check_dnsmasq_nftset(), + smartdns_installed: env.smartdns_installed, + smartdns_ipset_support: env.smartdns_installed && env.ipset_supported, + smartdns_nftset_support: env.smartdns_installed && env.nft_installed, + unbound_installed: env.unbound_installed, + leds: lsdir('/sys/class/leds') || [], + }, + + // File URL sizes (from procd data, pre-computed at start time) + file_url: svc_data?.file_url || [], + }; + return result; +} + +function get_init_list(name) { + name = name || pkg.name; + let result = {}; + let enabled_val = (uci(pkg.name).get(pkg.name, 'config', 'enabled') ?? '0'); + result[name] = { enabled: (enabled_val == '1') }; + return result; +} + +function get_platform_support(name) { + name = name || pkg.name; + env.detect(); + let result = {}; + result[name] = { + ipset_installed: env.ipset_supported, + nft_installed: env.nft_installed, + dnsmasq_installed: env.dnsmasq_installed, + dnsmasq_ipset_support: env.check_dnsmasq_ipset(), + dnsmasq_nftset_support: env.check_dnsmasq_nftset(), + smartdns_installed: env.smartdns_installed, + smartdns_ipset_support: env.smartdns_installed && env.ipset_supported, + smartdns_nftset_support: env.smartdns_installed && env.nft_installed, + unbound_installed: env.unbound_installed, + leds: length(lsdir('/sys/class/leds') || []) > 0, + }; + return result; +} + +function get_file_url_filesizes(name) { + name = name || pkg.name; + env.load('rpcd'); + + let files = []; + uci(pkg.name).foreach(pkg.name, 'file_url', (s) => { + let size = s.size; + if (!size && s.url) size = get_url_filesize(s.url); + push(files, { name: s.name || s.url, url: s.url, size: size || '' }); + }); + + let result = {}; + result[name] = { file_url: files }; + return result; +} + +// ── Module Init & Export ──────────────────────────────────────────── + +function set_script_name(name) { + state.script_name = name; +} + +export default { + init: function() {}, // backward compat (rpcd plugin may still call this) + set_script_name, + pkg, + + // Core lifecycle + env, + start, + stop, + status_service, + + // Config + load_dl_command, + adb_config_update, + adb_config_cache, + + // Extra commands + allow, + check, + check_tld, + check_leading_dot, + check_lists, + killcache, + pause, + show_blocklist, + sizes, + + // rpcd data + get_init_status, + get_init_list, + get_platform_support, + get_file_url_filesizes, + + // init script helpers + get_network_trigger_info, + dl, + emit_procd_shell, + process_file_url, +}; + diff --git a/net/adblock-fast/files/lib/adblock-fast/cli.uc b/net/adblock-fast/files/lib/adblock-fast/cli.uc new file mode 100644 index 0000000000..6c63145315 --- /dev/null +++ b/net/adblock-fast/files/lib/adblock-fast/cli.uc @@ -0,0 +1,95 @@ +'use strict'; +// SPDX-License-Identifier: AGPL-3.0-or-later +// Copyright 2023-2026 MOSSDeF, Stan Grishin (stangri@melmac.ca). +// +// CLI dispatcher for adblock-fast. +// Called from init script: +// ucode -S -L /lib/adblock-fast /lib/adblock-fast/cli.uc -- [args...] + +import adb from 'adblock-fast'; + +let action = shift(ARGV); +if (action == '--') action = shift(ARGV); + +switch (action) { +case 'start': + let start_result = adb.start(ARGV); + if (start_result) + print(adb.emit_procd_shell(start_result)); + break; + +case 'stop': + let stop_result = adb.stop(); + if (stop_result) + print(adb.emit_procd_shell(stop_result)); + break; + +case 'status': + adb.status_service(ARGV[0]); + break; + +case 'allow': + adb.allow(join(' ', ARGV)); + break; + +case 'check': + adb.check(join(' ', ARGV)); + break; + +case 'check_tld': + adb.check_tld(); + break; + +case 'check_leading_dot': + adb.check_leading_dot(); + break; + +case 'check_lists': + adb.check_lists(join(' ', ARGV)); + break; + +case 'dl': + let dl_result = adb.dl(); + if (dl_result) + print(adb.emit_procd_shell(dl_result)); + break; + +case 'killcache': + adb.killcache(); + break; + +case 'pause': + adb.pause(ARGV[0]); + break; + +case 'show_blocklist': + adb.show_blocklist(); + break; + +case 'sizes': + adb.sizes(); + break; + +case 'version': + print(adb.pkg.version + '\n'); + break; + +case 'get_wan_interfaces': + let info = adb.get_network_trigger_info(); + if (info) + print(sprintf('%J', info) + '\n'); + break; + +case 'adb_config_update': + adb.adb_config_update(ARGV[0]); + break; + +case 'load_environment': + let env_ok = adb.env.load(ARGV[0], ARGV[1]); + exit(env_ok ? 0 : 1); + break; + +default: + warn('Unknown action: ' + (action || '(none)') + '\n'); + exit(1); +} diff --git a/net/adblock-fast/tests/01_pipeline/01_all_dns_modes b/net/adblock-fast/tests/01_pipeline/01_all_dns_modes new file mode 100644 index 0000000000..66de03b00d --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/01_all_dns_modes @@ -0,0 +1,193 @@ +Test that all 9 DNS modes produce valid output files containing +domains from both the domains.txt and hosts.txt test data files. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile, dirname, mkdir } from 'fs'; +let ti = adb._test_internals; + +let modes = [ + 'dnsmasq.servers', + 'dnsmasq.conf', + 'dnsmasq.ipset', + 'dnsmasq.nftset', + 'dnsmasq.addnhosts', + 'smartdns.domainset', + 'smartdns.ipset', + 'smartdns.nftset', + 'unbound.adb_list', +]; + +// Known-good domains that MUST appear in every output +let must_have = [ + 'ad.doubleclick.test.example.com', + 'tracker.analytics.test.example.com', + 'common-shared-1.test.example.com', + 'common-shared-10.test.example.com', + 'adhost-zero-1.test.example.org', + 'adhost-loopback-1.test.example.org', + 'parent-dedup-1.test.example.com', +]; + +// Domains that MUST NOT appear (invalid entries or subdomain-deduped) +let must_not_have = [ + 'localhost', + 'nodot', + 'child.parent-dedup-1.test.example.com', + 'sub.child.parent-dedup-2.test.example.com', + 'deep.sub.parent-dedup-3.test.example.com', +]; + +let results = []; + +for (let mode in modes) { + // Reset module state for each mode + ti.env._config_loaded = false; + ti.env._loaded = false; + ti.env.dnsmasq_features = ''; + ti.env._detected = false; + ti.env.dnsmasq_ubus = null; + ti.status_data.errors = []; + ti.status_data.warnings = []; + ti.status_data.status = ''; + ti.status_data.message = ''; + ti.status_data.stats = ''; + + // Load config from mock UCI + adb.env.load_config(); + + // Override DNS mode via set_cfg (load() reassigns cfg, so direct ref is stale) + ti.set_cfg('dns', mode); + ti.set_cfg('enabled', true); + ti.set_cfg('dnsmasq_sanity_check', false); + ti.set_cfg('dnsmasq_validity_check', false); + ti.set_cfg('heartbeat_domain', null); + ti.set_cfg('config_update_enabled', false); + ti.set_cfg('update_config_sizes', false); + ti.env.dns_set_output_values(mode); + + // Collect file_url sections + ti.append_urls(); + + // Ensure output directory exists + let out_file = ti.dns_output.file; + let out_dir = dirname(out_file); + mkdir(out_dir); + + // Run the download and processing pipeline + let ok = ti.download_lists(); + + if (!ok) { + push(results, sprintf('%s: FAIL (download_lists returned false)', mode)); + if (length(ti.status_data.errors)) + push(results, sprintf(' errors: %J', ti.status_data.errors)); + continue; + } + + // Read output + let content = readfile(out_file); + if (!content || !length(content)) { + push(results, sprintf('%s: FAIL (empty output file %s)', mode, out_file)); + continue; + } + + let lines = filter(split(content, '\n'), l => length(l) > 0); + let line_count = length(lines); + + // Extract domains using the mode's parse_filter + let dm = ti.dns_modes[mode]; + let domains = {}; + let bad_format = 0; + + for (let line in lines) { + let domain; + switch (mode) { + case 'dnsmasq.servers': + let m1 = match(line, /^server=\/([^\/]+)\/$/); + domain = m1 ? m1[1] : null; + break; + case 'dnsmasq.conf': + let m2 = match(line, /^local=\/([^\/]+)\/$/); + domain = m2 ? m2[1] : null; + break; + case 'dnsmasq.ipset': + let m3 = match(line, /^ipset=\/([^\/]+)\/adb$/); + domain = m3 ? m3[1] : null; + break; + case 'dnsmasq.nftset': + let m4 = match(line, /^nftset=\/([^\/]+)\/4#/); + domain = m4 ? m4[1] : null; + break; + case 'dnsmasq.addnhosts': + let m5 = match(line, /^127\.0\.0\.1 (.+)$/); + domain = m5 ? m5[1] : null; + break; + case 'smartdns.domainset': + case 'smartdns.ipset': + case 'smartdns.nftset': + domain = match(line, /^[a-zA-Z0-9._-]+$/) ? line : null; + break; + case 'unbound.adb_list': + let m6 = match(line, /^local-zone: "([^"]+)\." always_nxdomain$/); + domain = m6 ? m6[1] : null; + if (!domain && line == 'server:') domain = '__header__'; + break; + } + if (domain && domain != '__header__') + domains[domain] = true; + else if (!domain) + bad_format++; + } + + let domain_count = length(keys(domains)); + + // Check must_have domains + let missing = []; + for (let d in must_have) { + if (!domains[d]) + push(missing, d); + } + + // Check must_not_have domains + let unwanted = []; + for (let d in must_not_have) { + if (domains[d]) + push(unwanted, d); + } + + // dnsmasq.addnhosts doesn't do subdomain dedup (not in needs_optimization list) + let skip_dedup = (mode == 'dnsmasq.addnhosts'); + if (skip_dedup) { + unwanted = filter(unwanted, d => + d != 'child.parent-dedup-1.test.example.com' && + d != 'sub.child.parent-dedup-2.test.example.com' && + d != 'deep.sub.parent-dedup-3.test.example.com'); + } + + if (length(missing) == 0 && length(unwanted) == 0 && bad_format == 0 && domain_count > 100) + push(results, sprintf('%s: PASS (%d domains)', mode, domain_count)); + else { + let detail = sprintf('%s: FAIL (%d domains, %d bad_format', mode, domain_count, bad_format); + if (length(missing)) + detail += sprintf(', missing: %J', missing); + if (length(unwanted)) + detail += sprintf(', unwanted: %J', unwanted); + detail += ')'; + push(results, detail); + } +} + +print(join('\n', results) + '\n'); +-- End -- + +-- Expect stdout -- +dnsmasq.servers: PASS (162 domains) +dnsmasq.conf: PASS (162 domains) +dnsmasq.ipset: PASS (162 domains) +dnsmasq.nftset: PASS (162 domains) +dnsmasq.addnhosts: PASS (165 domains) +smartdns.domainset: PASS (162 domains) +smartdns.ipset: PASS (162 domains) +smartdns.nftset: PASS (162 domains) +unbound.adb_list: PASS (162 domains) +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/02_input_format_detection b/net/adblock-fast/tests/01_pipeline/02_input_format_detection new file mode 100644 index 0000000000..a210c726bc --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/02_input_format_detection @@ -0,0 +1,42 @@ +Test that detect_file_type() correctly identifies all supported list formats. + +-- Testcase -- +import adb from 'adblock-fast'; +import { writefile, mkdir } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); + +let test_dir = '' + TESTDIR + '/fmt_test'; +mkdir(test_dir); + +let tests = [ + ['domains', 'example.com\nad.tracker.net\nmalware.bad.org\n'], + ['hosts', '0.0.0.0 example.com\n127.0.0.1 tracker.net\n0.0.0.0 malware.org\n'], + ['adblockplus', '[Adblock Plus]\n||example.com^\n||tracker.net^\n'], + ['dnsmasq', 'server=/example.com/\nserver=/tracker.net/\n'], +]; + +let results = []; + +for (let t in tests) { + let name = t[0]; + let content = t[1]; + let path = test_dir + '/' + name + '.txt'; + writefile(path, content); + let detected = ti.detect_file_type(path); + if (detected == name) + push(results, sprintf('%s: PASS', name)); + else + push(results, sprintf('%s: FAIL (detected as %s)', name, detected)); +} + +print(join('\n', results) + '\n'); +-- End -- + +-- Expect stdout -- +domains: PASS +hosts: PASS +adblockplus: PASS +dnsmasq: PASS +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/03_subdomain_dedup b/net/adblock-fast/tests/01_pipeline/03_subdomain_dedup new file mode 100644 index 0000000000..b0fde8790d --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/03_subdomain_dedup @@ -0,0 +1,58 @@ +Test that subdomain dedup removes child domains when parent exists. +Parent domains are in domains.txt, children are in hosts.txt. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + + // Parents must exist + let parents = [ + 'parent-dedup-1.test.example.com', + 'parent-dedup-2.test.example.com', + 'parent-dedup-3.test.example.com', + ]; + // Children must be removed + let children = [ + 'child.parent-dedup-1.test.example.com', + 'sub.child.parent-dedup-2.test.example.com', + 'deep.sub.parent-dedup-3.test.example.com', + ]; + + let results = []; + for (let p in parents) { + let found = index(content, 'server=/' + p + '/') >= 0; + push(results, sprintf('parent %s: %s', p, found ? 'PRESENT' : 'MISSING')); + } + for (let c in children) { + let found = index(content, 'server=/' + c + '/') >= 0; + push(results, sprintf('child %s: %s', c, found ? 'PRESENT (BAD)' : 'REMOVED')); + } + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +parent parent-dedup-1.test.example.com: PRESENT +parent parent-dedup-2.test.example.com: PRESENT +parent parent-dedup-3.test.example.com: PRESENT +child child.parent-dedup-1.test.example.com: REMOVED +child sub.child.parent-dedup-2.test.example.com: REMOVED +child deep.sub.parent-dedup-3.test.example.com: REMOVED +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/04_allowed_domains b/net/adblock-fast/tests/01_pipeline/04_allowed_domains new file mode 100644 index 0000000000..a54d03f7da --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/04_allowed_domains @@ -0,0 +1,55 @@ +Test that allowed_domain config option removes domains and subdomains from output. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.set_cfg('allowed_domain', 'ad.doubleclick.test.example.com tracker.analytics.test.example.com'); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + + // These should be removed by allowed_domain + let allowed = [ + 'ad.doubleclick.test.example.com', + 'tracker.analytics.test.example.com', + ]; + // This should still be present + let blocked = [ + 'pixel.tracking.test.example.com', + 'beacon.metrics.test.example.com', + ]; + + let results = []; + for (let a in allowed) { + // Check for block format "server=/domain/\n" — not allow format "server=/domain/#\n" + let found = index(content, 'server=/' + a + '/\n') >= 0; + push(results, sprintf('allowed %s: %s', a, found ? 'STILL PRESENT (BAD)' : 'REMOVED')); + } + for (let b in blocked) { + let found = index(content, 'server=/' + b + '/\n') >= 0; + push(results, sprintf('blocked %s: %s', b, found ? 'PRESENT' : 'MISSING')); + } + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +allowed ad.doubleclick.test.example.com: REMOVED +allowed tracker.analytics.test.example.com: REMOVED +blocked pixel.tracking.test.example.com: PRESENT +blocked beacon.metrics.test.example.com: PRESENT +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/05_canary_domains b/net/adblock-fast/tests/01_pipeline/05_canary_domains new file mode 100644 index 0000000000..f1944d45ab --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/05_canary_domains @@ -0,0 +1,45 @@ +Test that canary domains are injected when enabled. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.set_cfg('canary_domains_icloud', true); +ti.set_cfg('canary_domains_mozilla', true); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + + let canary_domains = [ + 'mask.icloud.com', + 'mask-h2.icloud.com', + 'use-application-dns.net', + ]; + + let results = []; + for (let d in canary_domains) { + let found = index(content, 'server=/' + d + '/') >= 0; + push(results, sprintf('canary %s: %s', d, found ? 'PRESENT' : 'MISSING')); + } + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +canary mask.icloud.com: PRESENT +canary mask-h2.icloud.com: PRESENT +canary use-application-dns.net: PRESENT +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/06_servers_mode_allow b/net/adblock-fast/tests/01_pipeline/06_servers_mode_allow new file mode 100644 index 0000000000..561781a5c0 --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/06_servers_mode_allow @@ -0,0 +1,57 @@ +Test that dnsmasq.servers mode prepends explicit allow entries (server=/domain/#) +when allowed_domain is set. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.set_cfg('allowed_domain', 'safe.example.com also-safe.example.com'); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + let lines = filter(split(content, '\n'), l => length(l) > 0); + + // In servers mode with allowed_domain, server=/domain/# entries are prepended + let allow_entries = filter(lines, l => match(l, /\/#$/)); + let block_entries = filter(lines, l => match(l, /\/$/)); + + let results = []; + push(results, sprintf('allow_entries: %d', length(allow_entries))); + push(results, sprintf('block_entries: %d', length(block_entries))); + + // Check specific allow entries + let has_safe = index(content, 'server=/safe.example.com/#') >= 0; + let has_also = index(content, 'server=/also-safe.example.com/#') >= 0; + push(results, sprintf('safe.example.com allow: %s', has_safe ? 'PRESENT' : 'MISSING')); + push(results, sprintf('also-safe.example.com allow: %s', has_also ? 'PRESENT' : 'MISSING')); + + // Allow entries should be at the top + if (length(allow_entries) > 0 && length(lines) > 0) { + let first_is_allow = match(lines[0], /\/#$/); + push(results, sprintf('allow_entries_at_top: %s', first_is_allow ? 'YES' : 'NO')); + } + + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +allow_entries: 2 +block_entries: 162 +safe.example.com allow: PRESENT +also-safe.example.com allow: PRESENT +allow_entries_at_top: YES +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/07_ipv6_addnhosts b/net/adblock-fast/tests/01_pipeline/07_ipv6_addnhosts new file mode 100644 index 0000000000..df36d90944 --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/07_ipv6_addnhosts @@ -0,0 +1,50 @@ +Test that dnsmasq.addnhosts with ipv6_enabled produces both +127.0.0.1 and :: entries (dual-stack). + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.addnhosts'); +ti.set_cfg('ipv6_enabled', true); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.env.dns_set_output_values('dnsmasq.addnhosts'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + let lines = filter(split(content, '\n'), l => length(l) > 0); + + let ipv4_lines = filter(lines, l => match(l, /^127\.0\.0\.1 /)); + let ipv6_lines = filter(lines, l => match(l, /^:: /)); + + let results = []; + push(results, sprintf('ipv4_count: %d', length(ipv4_lines))); + push(results, sprintf('ipv6_count: %d', length(ipv6_lines))); + push(results, sprintf('counts_match: %s', length(ipv4_lines) == length(ipv6_lines) ? 'YES' : 'NO')); + + // Spot-check a specific domain appears in both + let test_domain = 'ad.doubleclick.test.example.com'; + let has_ipv4 = index(content, '127.0.0.1 ' + test_domain) >= 0; + let has_ipv6 = index(content, ':: ' + test_domain) >= 0; + push(results, sprintf('dual_stack_%s: %s', test_domain, (has_ipv4 && has_ipv6) ? 'YES' : 'NO')); + + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +ipv4_count: 165 +ipv6_count: 165 +counts_match: YES +dual_stack_ad.doubleclick.test.example.com: YES +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/08_ipv6_nftset b/net/adblock-fast/tests/01_pipeline/08_ipv6_nftset new file mode 100644 index 0000000000..1c36d1786f --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/08_ipv6_nftset @@ -0,0 +1,54 @@ +Test that dnsmasq.nftset with ipv6_enabled includes IPv6 set reference. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.nftset'); +ti.set_cfg('ipv6_enabled', true); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.env.dns_set_output_values('dnsmasq.nftset'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + let lines = filter(split(content, '\n'), l => length(l) > 0); + + // With IPv6 enabled, nftset format should include ,6#inet#fw4#adb6 + let ipv6_lines = filter(lines, l => match(l, /6#inet#fw4#adb6/)); + let ipv4_only = filter(lines, l => match(l, /4#inet#fw4#adb4/) && !match(l, /6#inet#fw4#adb6/)); + + let results = []; + push(results, sprintf('total_lines: %d', length(lines))); + push(results, sprintf('with_ipv6_set: %d', length(ipv6_lines))); + push(results, sprintf('ipv4_only: %d', length(ipv4_only))); + + // All lines should have both IPv4 and IPv6 set references + push(results, sprintf('all_dual_stack: %s', (length(ipv6_lines) == length(lines)) ? 'YES' : 'NO')); + + // Spot-check format + let test_domain = 'ad.doubleclick.test.example.com'; + let expected = 'nftset=/' + test_domain + '/4#inet#fw4#adb4,6#inet#fw4#adb6'; + let has_entry = index(content, expected) >= 0; + push(results, sprintf('correct_format: %s', has_entry ? 'YES' : 'NO')); + + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +total_lines: 162 +with_ipv6_set: 162 +ipv4_only: 0 +all_dual_stack: YES +correct_format: YES +-- End -- diff --git a/net/adblock-fast/tests/01_pipeline/09_unbound_header b/net/adblock-fast/tests/01_pipeline/09_unbound_header new file mode 100644 index 0000000000..dc73b1fff9 --- /dev/null +++ b/net/adblock-fast/tests/01_pipeline/09_unbound_header @@ -0,0 +1,48 @@ +Test that unbound.adb_list mode prepends "server:" header line. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'unbound.adb_list'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.env.dns_set_output_values('unbound.adb_list'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + let lines = filter(split(content, '\n'), l => length(l) > 0); + + let results = []; + + // First line should be "server:" + push(results, sprintf('first_line: %s', lines[0])); + + // Rest should be local-zone entries + let lz_lines = filter(lines, l => match(l, /^local-zone: /)); + push(results, sprintf('local_zone_entries: %d', length(lz_lines))); + + // Spot-check format + let test_domain = 'ad.doubleclick.test.example.com'; + let expected = 'local-zone: "' + test_domain + '." always_nxdomain'; + let has_entry = index(content, expected) >= 0; + push(results, sprintf('correct_format: %s', has_entry ? 'YES' : 'NO')); + + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +first_line: server: +local_zone_entries: 162 +correct_format: YES +-- End -- diff --git a/net/adblock-fast/tests/02_config/01_blocked_domain_injection b/net/adblock-fast/tests/02_config/01_blocked_domain_injection new file mode 100644 index 0000000000..0caf3e069a --- /dev/null +++ b/net/adblock-fast/tests/02_config/01_blocked_domain_injection @@ -0,0 +1,41 @@ +Test that cfg.blocked_domain entries are added to the output. + +-- Testcase -- +import adb from 'adblock-fast'; +import { readfile } from 'fs'; +let ti = adb._test_internals; + +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.set_cfg('blocked_domain', 'custom-block-1.example.net custom-block-2.example.net'); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); + +let ok = ti.download_lists(); +if (!ok) { + print('download_lists failed\n'); +} else { + let content = readfile(ti.dns_output.file) || ''; + + let customs = [ + 'custom-block-1.example.net', + 'custom-block-2.example.net', + ]; + let results = []; + for (let d in customs) { + let found = index(content, 'server=/' + d + '/') >= 0; + push(results, sprintf('%s: %s', d, found ? 'PRESENT' : 'MISSING')); + } + print(join('\n', results) + '\n'); +} +-- End -- + +-- Expect stdout -- +custom-block-1.example.net: PRESENT +custom-block-2.example.net: PRESENT +-- End -- diff --git a/net/adblock-fast/tests/03_functional/01_check_domain b/net/adblock-fast/tests/03_functional/01_check_domain new file mode 100644 index 0000000000..9b013eb82e --- /dev/null +++ b/net/adblock-fast/tests/03_functional/01_check_domain @@ -0,0 +1,33 @@ +Test that check() correctly identifies blocked and unblocked domains. +check() output goes to stderr via output.info/output.print when is_tty=true. + +-- Testcase -- +import adb from 'adblock-fast'; +let ti = adb._test_internals; + +// Build the blocklist +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); +ti.download_lists(); + +// Enable tty mode so output goes to stderr (captured by test framework) +// Use verbosity=2 to get verbose output (which includes [PROC] prefix) +ti.state.is_tty = true; +ti.set_cfg('verbosity', 2); + +// Now test check() +adb.check('ad.doubleclick.test.example.com this-domain-not-in-list.test.example.com'); +-- End -- + +-- Expect stderr -- +[PROC] Found 1 match for 'ad.doubleclick.test.example.com' in 'TESTDIR/var_run/adblock-fast/dnsmasq.servers'. +ad.doubleclick.test.example.com +[PROC] The 'this-domain-not-in-list.test.example.com' is not found in current block-list ('TESTDIR/var_run/adblock-fast/dnsmasq.servers'). +-- End -- diff --git a/net/adblock-fast/tests/03_functional/02_show_blocklist b/net/adblock-fast/tests/03_functional/02_show_blocklist new file mode 100644 index 0000000000..52f877a551 --- /dev/null +++ b/net/adblock-fast/tests/03_functional/02_show_blocklist @@ -0,0 +1,53 @@ +Test that show_blocklist() outputs parsed domains to stdout. + +-- Testcase -- +import adb from 'adblock-fast'; +import { popen } from 'fs'; +let ti = adb._test_internals; + +// Build the blocklist +adb.env.load_config(); +ti.set_cfg('dns', 'dnsmasq.servers'); +ti.set_cfg('dnsmasq_sanity_check', false); +ti.set_cfg('dnsmasq_validity_check', false); +ti.set_cfg('heartbeat_domain', null); +ti.set_cfg('config_update_enabled', false); +ti.set_cfg('update_config_sizes', false); +ti.env.dns_set_output_values('dnsmasq.servers'); +ti.append_urls(); +ti.download_lists(); + +// show_blocklist() uses system() to run sed parse_filter on the output file, +// which outputs plain domains to stdout. Count lines and check a few domains. +let cmd = sprintf("sed '%s' %s", ti.dns_output.parse_filter, ti.dns_output.file); +let p = popen(cmd, 'r'); +let out = p ? (p.read('all') || '') : ''; +if (p) p.close(); + +let domains = filter(split(out, '\n'), l => length(l) > 0); +let count = length(domains); + +let must_have = [ + 'ad.doubleclick.test.example.com', + 'common-shared-1.test.example.com', + 'adhost-zero-1.test.example.org', +]; + +let domain_set = {}; +for (let d in domains) domain_set[d] = true; + +let results = []; +push(results, sprintf('domain_count: %d', count)); +for (let d in must_have) { + push(results, sprintf('%s: %s', d, domain_set[d] ? 'PRESENT' : 'MISSING')); +} + +print(join('\n', results) + '\n'); +-- End -- + +-- Expect stdout -- +domain_count: 162 +ad.doubleclick.test.example.com: PRESENT +common-shared-1.test.example.com: PRESENT +adhost-zero-1.test.example.org: PRESENT +-- End -- diff --git a/net/adblock-fast/tests/data/adblockplus.txt b/net/adblock-fast/tests/data/adblockplus.txt new file mode 100644 index 0000000000..f2203da06e --- /dev/null +++ b/net/adblock-fast/tests/data/adblockplus.txt @@ -0,0 +1,26 @@ +[Adblock Plus 2.0] +! Title: Test ABP blocklist +! Last modified: 2024-01-01 +! Homepage: https://test.example.com + +||abp-tracker-1.test.example.com^ +||abp-tracker-2.test.example.com^ +||abp-tracker-3.test.example.com^ +||abp-tracker-4.test.example.com^ +||abp-tracker-5.test.example.com^ +||abp-tracker-6.test.example.com^ +||abp-tracker-7.test.example.com^ +||abp-tracker-8.test.example.com^ +||abp-tracker-9.test.example.com^ +||abp-tracker-10.test.example.com^ +||abp-tracker-11.test.example.com^$third-party +||abp-tracker-12.test.example.com^$third-party +||abp-tracker-13.test.example.com^ +||abp-tracker-14.test.example.com^ +||abp-tracker-15.test.example.com^ +! Comment between entries +||abp-tracker-16.test.example.com^ +||abp-tracker-17.test.example.com^ +||abp-tracker-18.test.example.com^ +||abp-tracker-19.test.example.com^ +||abp-tracker-20.test.example.com^ diff --git a/net/adblock-fast/tests/data/allowed.txt b/net/adblock-fast/tests/data/allowed.txt new file mode 100644 index 0000000000..de12cea0bc --- /dev/null +++ b/net/adblock-fast/tests/data/allowed.txt @@ -0,0 +1,6 @@ +# Allowed domains list (hosts format, used with action=allow) +0.0.0.0 adhost-zero-1.test.example.org +0.0.0.0 adhost-zero-2.test.example.org +0.0.0.0 adhost-zero-3.test.example.org +0.0.0.0 common-shared-1.test.example.com +0.0.0.0 common-shared-2.test.example.com diff --git a/net/adblock-fast/tests/data/dnsmasq_servers.txt b/net/adblock-fast/tests/data/dnsmasq_servers.txt new file mode 100644 index 0000000000..26fb4e8694 --- /dev/null +++ b/net/adblock-fast/tests/data/dnsmasq_servers.txt @@ -0,0 +1,10 @@ +server=/dnsmasq-input-1.test.example.com/ +server=/dnsmasq-input-2.test.example.com/ +server=/dnsmasq-input-3.test.example.com/ +server=/dnsmasq-input-4.test.example.com/ +server=/dnsmasq-input-5.test.example.com/ +server=/dnsmasq-input-6.test.example.com/ +server=/dnsmasq-input-7.test.example.com/ +server=/dnsmasq-input-8.test.example.com/ +server=/dnsmasq-input-9.test.example.com/ +server=/dnsmasq-input-10.test.example.com/ diff --git a/net/adblock-fast/tests/data/domains.txt b/net/adblock-fast/tests/data/domains.txt new file mode 100644 index 0000000000..27bef176bd --- /dev/null +++ b/net/adblock-fast/tests/data/domains.txt @@ -0,0 +1,120 @@ +# Test domain blocklist for adblock-fast functional tests +# Lines starting with # are comments and should be filtered out + +# -- Valid domains (85 unique) -- +ad.doubleclick.test.example.com +ads.bigadnetwork.test.example.com +tracker.analytics.test.example.com +pixel.tracking.test.example.com +beacon.metrics.test.example.com +telemetry.data.test.example.com +stats.counter.test.example.com +banner.display.test.example.com +popup.overlay.test.example.com +interstitial.fullpage.test.example.com +video.preroll.test.example.com +native.sponsored.test.example.com +widget.recommendation.test.example.com +feed.promoted.test.example.com +sidebar.adunit.test.example.com +footer.adzone.test.example.com +header.leaderboard.test.example.com +skyscraper.tower.test.example.com +rectangle.medium.test.example.com +billboard.jumbo.test.example.com +expandable.rich.test.example.com +floating.sticky.test.example.com +adhesion.anchor.test.example.com +pushdown.slide.test.example.com +catfish.bottom.test.example.com +curtain.takeover.test.example.com +skin.wrap.test.example.com +roadblock.wall.test.example.com +splash.welcome.test.example.com +exit.intent.test.example.com +retarget.remarket.test.example.com +lookalike.audience.test.example.com +segment.profile.test.example.com +cookie.sync.test.example.com +fingerprint.device.test.example.com +supercookie.persist.test.example.com +evercookie.track.test.example.com +canvas.fp.test.example.com +webgl.hash.test.example.com +audio.context.test.example.com +font.enum.test.example.com +plugin.detect.test.example.com +screen.res.test.example.com +timezone.offset.test.example.com +language.pref.test.example.com +dnt.ignore.test.example.com +referer.leak.test.example.com +click.redirect.test.example.com +impression.log.test.example.com +conversion.pixel.test.example.com +attribution.track.test.example.com +viewability.measure.test.example.com +brand.safety.test.example.com +fraud.detect.test.example.com +bot.filter.test.example.com +programmatic.bid.test.example.com +realtime.auction.test.example.com +demand.side.test.example.com +supply.platform.test.example.com +exchange.market.test.example.com +mediation.waterfall.test.example.com +prebid.header.test.example.com +openrtb.proto.test.example.com +vast.vpaid.test.example.com +mraid.sdk.test.example.com +gdpr.consent.test.example.com +ccpa.optout.test.example.com +tcf.vendor.test.example.com +cmp.dialog.test.example.com +ab.testing.test.example.com +multivariate.experiment.test.example.com +heatmap.session.test.example.com +scroll.depth.test.example.com +funnel.analysis.test.example.com +cohort.study.test.example.com +survey.feedback.test.example.com +nps.score.test.example.com +crm.integration.test.example.com +cdp.unify.test.example.com +dmp.segment.test.example.com +tag.manager.test.example.com +container.script.test.example.com + +# -- Domains shared with hosts.txt (10 overlapping) -- +common-shared-1.test.example.com +common-shared-2.test.example.com +common-shared-3.test.example.com +common-shared-4.test.example.com +common-shared-5.test.example.com +common-shared-6.test.example.com +common-shared-7.test.example.com +common-shared-8.test.example.com +common-shared-9.test.example.com +common-shared-10.test.example.com + +# -- Subdomain dedup test pairs (parent in this file) -- +parent-dedup-1.test.example.com +parent-dedup-2.test.example.com +parent-dedup-3.test.example.com + +# -- Invalid entries (should be filtered out) -- +# Comment only (filtered by /^#/d) +192.168.1.1 +10.0.0.1 +127.0.0.1 +0.0.0.0 +255.255.255.255 +-starts-with-dash.test.example.com +.starts-with-dot.test.example.com +ends-with-dot.test.example.com. +has..double.dots.test.example.com +has spaces in it.test.example.com +has@symbol.test.example.com +has!bang.test.example.com +nodot +just a random sentence diff --git a/net/adblock-fast/tests/data/hosts.txt b/net/adblock-fast/tests/data/hosts.txt new file mode 100644 index 0000000000..fab876b2b9 --- /dev/null +++ b/net/adblock-fast/tests/data/hosts.txt @@ -0,0 +1,108 @@ +# Test hosts blocklist for adblock-fast functional tests +# Title: StevenBlack Unified hosts (adware + malware) +# +# This block between "# Title: StevenBlack" and "# Custom host records" +# is specifically removed by adblock-fast (lines 1589-1590). +# Entries here should NOT appear in the output. +# stevenblack-should-not-appear.test.example.com +0.0.0.0 stevenblack-entry-hidden.test.example.com +# Custom host records are listed here + +# -- Standard localhost entries (should be filtered) -- +127.0.0.1 localhost +127.0.0.1 localhost.localdomain +127.0.0.1 local +0.0.0.0 0.0.0.0 +::1 localhost + +# -- Valid host entries with 0.0.0.0 prefix (40 unique) -- +0.0.0.0 adhost-zero-1.test.example.org +0.0.0.0 adhost-zero-2.test.example.org +0.0.0.0 adhost-zero-3.test.example.org +0.0.0.0 adhost-zero-4.test.example.org +0.0.0.0 adhost-zero-5.test.example.org +0.0.0.0 adhost-zero-6.test.example.org +0.0.0.0 adhost-zero-7.test.example.org +0.0.0.0 adhost-zero-8.test.example.org +0.0.0.0 adhost-zero-9.test.example.org +0.0.0.0 adhost-zero-10.test.example.org +0.0.0.0 adhost-zero-11.test.example.org +0.0.0.0 adhost-zero-12.test.example.org +0.0.0.0 adhost-zero-13.test.example.org +0.0.0.0 adhost-zero-14.test.example.org +0.0.0.0 adhost-zero-15.test.example.org +0.0.0.0 adhost-zero-16.test.example.org +0.0.0.0 adhost-zero-17.test.example.org +0.0.0.0 adhost-zero-18.test.example.org +0.0.0.0 adhost-zero-19.test.example.org +0.0.0.0 adhost-zero-20.test.example.org +0.0.0.0 adhost-zero-21.test.example.org +0.0.0.0 adhost-zero-22.test.example.org +0.0.0.0 adhost-zero-23.test.example.org +0.0.0.0 adhost-zero-24.test.example.org +0.0.0.0 adhost-zero-25.test.example.org +0.0.0.0 adhost-zero-26.test.example.org +0.0.0.0 adhost-zero-27.test.example.org +0.0.0.0 adhost-zero-28.test.example.org +0.0.0.0 adhost-zero-29.test.example.org +0.0.0.0 adhost-zero-30.test.example.org +0.0.0.0 adhost-zero-31.test.example.org +0.0.0.0 adhost-zero-32.test.example.org +0.0.0.0 adhost-zero-33.test.example.org +0.0.0.0 adhost-zero-34.test.example.org +0.0.0.0 adhost-zero-35.test.example.org +0.0.0.0 adhost-zero-36.test.example.org +0.0.0.0 adhost-zero-37.test.example.org +0.0.0.0 adhost-zero-38.test.example.org +0.0.0.0 adhost-zero-39.test.example.org +0.0.0.0 adhost-zero-40.test.example.org + +# -- Valid host entries with 127.0.0.1 prefix (25 unique) -- +127.0.0.1 adhost-loopback-1.test.example.org +127.0.0.1 adhost-loopback-2.test.example.org +127.0.0.1 adhost-loopback-3.test.example.org +127.0.0.1 adhost-loopback-4.test.example.org +127.0.0.1 adhost-loopback-5.test.example.org +127.0.0.1 adhost-loopback-6.test.example.org +127.0.0.1 adhost-loopback-7.test.example.org +127.0.0.1 adhost-loopback-8.test.example.org +127.0.0.1 adhost-loopback-9.test.example.org +127.0.0.1 adhost-loopback-10.test.example.org +127.0.0.1 adhost-loopback-11.test.example.org +127.0.0.1 adhost-loopback-12.test.example.org +127.0.0.1 adhost-loopback-13.test.example.org +127.0.0.1 adhost-loopback-14.test.example.org +127.0.0.1 adhost-loopback-15.test.example.org +127.0.0.1 adhost-loopback-16.test.example.org +127.0.0.1 adhost-loopback-17.test.example.org +127.0.0.1 adhost-loopback-18.test.example.org +127.0.0.1 adhost-loopback-19.test.example.org +127.0.0.1 adhost-loopback-20.test.example.org +127.0.0.1 adhost-loopback-21.test.example.org +127.0.0.1 adhost-loopback-22.test.example.org +127.0.0.1 adhost-loopback-23.test.example.org +127.0.0.1 adhost-loopback-24.test.example.org +127.0.0.1 adhost-loopback-25.test.example.org + +# -- Domains shared with domains.txt (10 overlapping) -- +0.0.0.0 common-shared-1.test.example.com +0.0.0.0 common-shared-2.test.example.com +0.0.0.0 common-shared-3.test.example.com +0.0.0.0 common-shared-4.test.example.com +0.0.0.0 common-shared-5.test.example.com +127.0.0.1 common-shared-6.test.example.com +127.0.0.1 common-shared-7.test.example.com +127.0.0.1 common-shared-8.test.example.com +127.0.0.1 common-shared-9.test.example.com +127.0.0.1 common-shared-10.test.example.com + +# -- Subdomain dedup test (children that should be removed when parent exists) -- +0.0.0.0 child.parent-dedup-1.test.example.com +0.0.0.0 sub.child.parent-dedup-2.test.example.com +0.0.0.0 deep.sub.parent-dedup-3.test.example.com + +# -- Invalid entries -- +some random text without IP prefix +0.0.0.0 +127.0.0.1 + diff --git a/net/adblock-fast/tests/lib/mocklib.uc b/net/adblock-fast/tests/lib/mocklib.uc new file mode 100644 index 0000000000..fd2bbf0da0 --- /dev/null +++ b/net/adblock-fast/tests/lib/mocklib.uc @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// Hybrid mocklib for adblock-fast functional tests. +// +// Key difference from mwan4's mocklib: +// - Does NOT mock the 'fs' module (real filesystem operations) +// - Selectively overrides system() to block service management +// while passing through data-processing commands (sed/sort/grep/awk) +// - Mocks only 'uci' and 'ubus' + +'use strict'; + +if (!exists(global, 'REQUIRE_SEARCH_PATH')) + global.REQUIRE_SEARCH_PATH = []; + +if (!exists(global, 'MOCK_SEARCH_PATH')) + global.MOCK_SEARCH_PATH = []; + +if (!exists(global, 'TRACE_CALLS')) + global.TRACE_CALLS = null; + +let _fs = require("fs"); + +// Force reloading uci and ubus modules so our mocks intercept them. +// Do NOT delete fs -- we want the REAL filesystem module. +delete global.modules.uci; +delete global.modules.ubus; + +let _log = (level, fmt, ...args) => { + let color, prefix; + + switch (level) { + case 'info': + color = 34; + prefix = '!'; + break; + case 'warn': + color = 33; + prefix = 'W'; + break; + case 'error': + color = 31; + prefix = 'E'; + break; + default: + color = 0; + prefix = 'I'; + } + + let f = sprintf("\u001b[%d;1m[%s] %s\u001b[0m", color, prefix, fmt); + warn(replace(sprintf(f, ...args), "\n", "\n "), "\n"); +}; + +let format_json = (data) => { + let rv; + + let format_value = (value) => { + switch (type(value)) { + case "object": + return sprintf("{ /* %d keys */ }", length(value)); + case "array": + return sprintf("[ /* %d items */ ]", length(value)); + case "string": + if (length(value) > 64) + value = substr(value, 0, 64) + "..."; + return sprintf("%J", value); + default: + return sprintf("%J", value); + } + }; + + switch (type(data)) { + case "object": + rv = "{"; + let k = sort(keys(data)); + for (let i, n in k) + rv += sprintf("%s %J: %s", i ? "," : "", n, format_value(data[n])); + rv += " }"; + break; + case "array": + rv = "["; + for (let i, v in data) + rv += (i ? "," : "") + " " + format_value(v); + rv += " ]"; + break; + default: + rv = format_value(data); + } + + return rv; +}; + +let read_data_file = (path) => { + for (let dir in MOCK_SEARCH_PATH) { + let fd = _fs.open(dir + '/' + path, "r"); + + if (fd) { + let data = fd.read("all"); + fd.close(); + return data; + } + } + + return null; +}; + +let read_json_file = (path) => { + let data = read_data_file(path); + + if (data != null) { + try { + return json(data); + } + catch (e) { + _log('error', "Unable to parse JSON data in %s: %s", path, e); + return NaN; + } + } + + return null; +}; + +let trace_call = (ns, func, args) => { + let msg = "[call] " + + (ns ? ns + "." : "") + + func; + + for (let k, v in args) { + msg += ' ' + k + ' <'; + + switch (type(v)) { + case "array": + case "object": + msg += format_json(v); + break; + default: + msg += v; + } + + msg += '>'; + } + + switch (TRACE_CALLS) { + case '1': + case 'stdout': + _fs.stdout.write(msg + "\n"); + break; + case 'stderr': + _fs.stderr.write(msg + "\n"); + break; + } +}; + +// Prepend mocklib/ to REQUIRE_SEARCH_PATH so mock uci/ubus are found +for (let pattern in REQUIRE_SEARCH_PATH) { + if (!match(pattern, /\*\.uc$/)) + continue; + + let path = replace(pattern, /\*/, 'mocklib'), + s = _fs.stat(path); + + if (!s || s.type != 'file') + continue; + + if (!length(global.MOCK_SEARCH_PATH)) + global.MOCK_SEARCH_PATH = [ replace(path, /mocklib\.uc$/, '../mocks') ]; + + unshift(REQUIRE_SEARCH_PATH, replace(path, /mocklib\.uc$/, 'mocklib/*.uc')); + break; +} + +if (!length(global.MOCK_SEARCH_PATH)) + global.MOCK_SEARCH_PATH = [ './mocks' ]; + +// Register global mocklib namespace +global.mocklib = { + require: function(module) { + let path, res, ex; + + if (type(REQUIRE_SEARCH_PATH) == "array" && index(REQUIRE_SEARCH_PATH[0], 'mocklib/*.uc') != -1) + path = shift(REQUIRE_SEARCH_PATH); + + try { + res = require(module); + } + catch (e) { + ex = e; + } + + if (path) + unshift(REQUIRE_SEARCH_PATH, path); + + if (ex) + die(ex); + + return res; + }, + + I: (...args) => _log('info', ...args), + N: (...args) => _log('notice', ...args), + W: (...args) => _log('warn', ...args), + E: (...args) => _log('error', ...args), + + format_json, + read_data_file, + read_json_file, + trace_call, +}; + +// Selectively override system() -- block service management, pass through data processing +global.system = ((_orig_system) => function(argv, timeout) { + let cmd = '' + argv; + + // Block commands that interact with system services or would hang + if (match(cmd, /^\/etc\/init\.d\//) || + match(cmd, /\/usr\/bin\/logger\b/) || + match(cmd, /^logger\b/) || + match(cmd, /^sleep\b/) || + match(cmd, /^resolveip\b/) || + match(cmd, /^dnsmasq\s+--test/) || + match(cmd, /^ipset\b/) || + match(cmd, /^nft\b/) || + match(cmd, /^chmod\b/) || + match(cmd, /^chown\b/)) { + trace_call(null, "system[blocked]", { command: cmd }); + return 0; + } + + // Pass through real commands for data processing + return _orig_system(argv, timeout); +})(global.system); + +// Override time() to return fixed value for reproducible tests +global.time = function() { + return 1615382640; +}; + +// Override getenv -- return null to prevent env interference +global.getenv = function(key) { + return null; +}; + +return global.mocklib; diff --git a/net/adblock-fast/tests/lib/mocklib/ubus.uc b/net/adblock-fast/tests/lib/mocklib/ubus.uc new file mode 100644 index 0000000000..ee6d487884 --- /dev/null +++ b/net/adblock-fast/tests/lib/mocklib/ubus.uc @@ -0,0 +1,69 @@ +// UBus mock for adblock-fast tests. +// Reused from mwan4's mock with no changes. + +let mocklib = global.mocklib; // ucode-lsp disable + +return { + connect: function() { + let self = this; + + return { + call: (object, method, args) => { + let signature = [ object + "~" + method ]; + + if (type(args) == "object") { + for (let i, k in sort(keys(args))) { + switch (type(args[k])) { + case "string": + case "double": + case "bool": + case "int": + push(signature, k + "-" + replace(args[k], /[^A-Za-z0-9_-]+/g, "_")); + break; + + default: + push(signature, type(args[k])); + } + } + } + + let candidates = []; + + for (let i = length(signature); i > 0; i--) { + let path = sprintf("ubus/%s.json", join("~", signature)), + mock = mocklib.read_json_file(path); + + if (mock != mock) { + self._error = "Invalid argument"; + + return null; + } + else if (mock) { + mocklib.trace_call("ctx", "call", { object, method, args }); + + return mock; + } + + push(candidates, path); + pop(signature); + } + + // Return null silently for unmatched calls (non-critical in tests) + self._error = "Method not found"; + + return null; + }, + + disconnect: () => null, + + error: () => self.error() + }; + }, + + error: function() { + let e = this._error; + delete this._error; + + return e; + } +}; diff --git a/net/adblock-fast/tests/lib/mocklib/uci.uc b/net/adblock-fast/tests/lib/mocklib/uci.uc new file mode 100644 index 0000000000..501ca1c92d --- /dev/null +++ b/net/adblock-fast/tests/lib/mocklib/uci.uc @@ -0,0 +1,213 @@ +// UCI mock for adblock-fast tests. +// Based on mwan4's UCI mock, extended with set/save/commit/changes/delete. + +let mocklib = global.mocklib; // ucode-lsp disable + +let byte = (str, off) => { // ucode-lsp disable + let v = ord(str, off); + return length(v) ? v[0] : v; +}; + +let hash = (s) => { // ucode-lsp disable + let h = 7; + + for (let i = 0; i < length(s); i++) + h = h * 31 + byte(s, i); + + return h; +}; + +let id = (config, t, n) => { // ucode-lsp disable + while (true) { + let id = sprintf('cfg%08x', hash(t + n)); + + if (!exists(config, id)) + return id; + + n++; + } +}; + +let fixup_config = (config) => { // ucode-lsp disable + let rv = {}; + let n_section = 0; + + for (let stype in config) { + switch (type(config[stype])) { + case 'object': + config[stype] = [ config[stype] ]; + /* fall through */ + + case 'array': + for (let idx, sobj in config[stype]) { + let sid, anon; + + if (exists(sobj, '.name') && !exists(rv, sobj['.name'])) { + sid = sobj['.name']; + anon = false; + } + else { + sid = id(rv, stype, idx); + anon = true; + } + + rv[sid] = { + '.index': n_section++, + ...sobj, + '.name': sid, + '.type': stype, + '.anonymous': anon + }; + } + + break; + } + } + + for (let n, sid in sort(keys(rv), (a, b) => rv[a]['.index'] - rv[b]['.index'])) + rv[sid]['.index'] = n; + + return rv; +}; + +return { + cursor: () => ({ + _configs: {}, + _dirty: {}, + + load: function(file) { + let basename = replace(file, /^.+\//, ''), + path = sprintf("uci/%s.json", basename), + mock = mocklib.read_json_file(path); + + if (!mock || mock != mock) { + mocklib.I("No configuration fixture defined for uci package %s.", file); + mocklib.I("Provide a mock configuration through the following JSON file:\n%s\n", path); + + return null; + } + + this._configs[basename] = fixup_config(mock); + }, + + _get_section: function(config, section) { + if (!exists(this._configs, config)) { + this.load(config); + + if (!exists(this._configs, config)) + return null; + } + + let cfg = this._configs[config], + extended = match(section, "^@([A-Za-z0-9_-]+)\\[(-?[0-9]+)\\]$"); + + if (extended) { + let stype = extended[1], + sindex = +extended[2]; + + let sids = sort( + filter(keys(cfg), sid => cfg[sid]['.type'] == stype), + (a, b) => cfg[a]['.index'] - cfg[b]['.index'] + ); + + if (sindex < 0) + sindex = sids.length + sindex; + + return cfg[sids[sindex]]; + } + + return cfg[section]; + }, + + get: function(config, section, option) { + let sobj = this._get_section(config, section); + + if (option && index(option, ".") == 0) + return null; + else if (sobj && option) + return sobj[option]; + else if (sobj) + return sobj[".type"]; + }, + + get_all: function(config, section) { + return section ? this._get_section(config, section) : this._configs[config]; + }, + + foreach: function(config, stype, cb) { + let rv = false; + + if (!exists(this._configs, config)) + this.load(config); + + if (exists(this._configs, config)) { + let cfg = this._configs[config], + sids = sort(keys(cfg), (a, b) => cfg[a]['.index'] - cfg[b]['.index']); + + for (let i, sid in sids) { + if (stype == null || cfg[sid]['.type'] == stype) { + if (cb({ ...(cfg[sid]) }) === false) + break; + + rv = true; + } + } + } + + return rv; + }, + + // -- Extensions for adblock-fast -- + + set: function(config, section, option, value) { + let sobj = this._get_section(config, section); + if (sobj) { + sobj[option] = value; + this._dirty[config] = true; + } + }, + + save: function(config) { + return true; + }, + + commit: function(config) { + delete this._dirty[config]; + return true; + }, + + changes: function(config) { + return this._dirty[config] ? [['set']] : []; + }, + + revert: function(config) { + delete this._dirty[config]; + }, + + delete: function(config, section, option) { + if (option) { + let sobj = this._get_section(config, section); + if (sobj) delete sobj[option]; + } else if (section) { + if (exists(this._configs, config)) + delete this._configs[config][section]; + } + }, + + list_add: function(config, section, option, value) { + let sobj = this._get_section(config, section); + if (!sobj) return; + if (type(sobj[option]) != 'array') + sobj[option] = sobj[option] ? [sobj[option]] : []; + push(sobj[option], value); + this._dirty[config] = true; + }, + + list_remove: function(config, section, option, value) { + let sobj = this._get_section(config, section); + if (!sobj || type(sobj[option]) != 'array') return; + sobj[option] = filter(sobj[option], v => v != value); + this._dirty[config] = true; + }, + }) +}; diff --git a/net/adblock-fast/tests/mocks/ubus/network.interface.wan~status.json b/net/adblock-fast/tests/mocks/ubus/network.interface.wan~status.json new file mode 100644 index 0000000000..2ae5bcc60b --- /dev/null +++ b/net/adblock-fast/tests/mocks/ubus/network.interface.wan~status.json @@ -0,0 +1,23 @@ +{ + "up": true, + "pending": false, + "available": true, + "autostart": true, + "device": "eth0", + "l3_device": "eth0", + "proto": "dhcp", + "ipv4-address": [ + { + "address": "192.168.1.100", + "mask": 24 + } + ], + "route": [ + { + "target": "0.0.0.0", + "mask": 0, + "nexthop": "192.168.1.1", + "source": "192.168.1.100/24" + } + ] +} diff --git a/net/adblock-fast/tests/mocks/ubus/network.interface~dump.json b/net/adblock-fast/tests/mocks/ubus/network.interface~dump.json new file mode 100644 index 0000000000..2d9b893b1f --- /dev/null +++ b/net/adblock-fast/tests/mocks/ubus/network.interface~dump.json @@ -0,0 +1,24 @@ +{ + "interface": [ + { + "interface": "loopback", + "up": true, + "device": "lo", + "l3_device": "lo", + "route": [] + }, + { + "interface": "wan", + "up": true, + "device": "eth0", + "l3_device": "eth0", + "route": [ + { + "target": "0.0.0.0", + "mask": 0, + "nexthop": "192.168.1.1" + } + ] + } + ] +} diff --git a/net/adblock-fast/tests/mocks/ubus/service~list~name-dnsmasq.json b/net/adblock-fast/tests/mocks/ubus/service~list~name-dnsmasq.json new file mode 100644 index 0000000000..212fb2e526 --- /dev/null +++ b/net/adblock-fast/tests/mocks/ubus/service~list~name-dnsmasq.json @@ -0,0 +1,11 @@ +{ + "dnsmasq": { + "instances": { + "cfg01411c": { + "running": true, + "pid": 1234, + "command": [ "/usr/sbin/dnsmasq", "-C", "/var/etc/dnsmasq.conf.cfg01411c", "-k", "-x", "/var/run/dnsmasq/dnsmasq.cfg01411c.pid" ] + } + } + } +} diff --git a/net/adblock-fast/tests/mocks/ubus/system~info.json b/net/adblock-fast/tests/mocks/ubus/system~info.json new file mode 100644 index 0000000000..c61c5b7471 --- /dev/null +++ b/net/adblock-fast/tests/mocks/ubus/system~info.json @@ -0,0 +1,17 @@ +{ + "memory": { + "total": 536870912, + "available": 268435456, + "free": 134217728, + "cached": 67108864, + "buffered": 33554432, + "shared": 4194304 + }, + "swap": { + "total": 0, + "free": 0 + }, + "uptime": 86400, + "localtime": 1615382640, + "load": [ 0, 0, 0 ] +} diff --git a/net/adblock-fast/tests/mocks/uci/adblock-fast.json b/net/adblock-fast/tests/mocks/uci/adblock-fast.json new file mode 100644 index 0000000000..e30ea2036e --- /dev/null +++ b/net/adblock-fast/tests/mocks/uci/adblock-fast.json @@ -0,0 +1,44 @@ +{ + "config": [ + { + ".name": "config", + ".type": "config", + "enabled": "1", + "dns": "dnsmasq.servers", + "verbosity": "2", + "force_dns": "0", + "compressed_cache": "0", + "config_update_enabled": "0", + "ipv6_enabled": "0", + "canary_domains_icloud": "0", + "canary_domains_mozilla": "0", + "dnsmasq_sanity_check": "0", + "dnsmasq_validity_check": "0", + "parallel_downloads": "0", + "allow_non_ascii": "0", + "update_config_sizes": "0", + "heartbeat_domain": "-", + "download_timeout": "10", + "curl_retry": "1", + "compressed_cache_dir": "TESTDIR/cache" + } + ], + "file_url": [ + { + ".name": "blocked_domains", + ".type": "file_url", + "enabled": "1", + "url": "file://TESTDIR/data/domains.txt", + "action": "block", + "name": "Test Domains" + }, + { + ".name": "blocked_hosts", + ".type": "file_url", + "enabled": "1", + "url": "file://TESTDIR/data/hosts.txt", + "action": "block", + "name": "Test Hosts" + } + ] +} diff --git a/net/adblock-fast/tests/mocks/uci/dhcp.json b/net/adblock-fast/tests/mocks/uci/dhcp.json new file mode 100644 index 0000000000..d800ffad66 --- /dev/null +++ b/net/adblock-fast/tests/mocks/uci/dhcp.json @@ -0,0 +1,23 @@ +{ + "dnsmasq": [ + { + ".name": "cfg01411c", + ".type": "dnsmasq", + ".anonymous": true, + "domainneeded": "1", + "localise_queries": "1", + "rebind_protection": "1", + "rebind_localhost": "1", + "local": "/lan/", + "domain": "lan", + "expandhosts": "1", + "cachesize": "1000", + "authoritative": "1", + "readethers": "1", + "leasefile": "/tmp/dhcp.leases", + "resolvfile": "/tmp/resolv.conf.d/resolv.conf.auto", + "localservice": "1", + "ednspacket_max": "1232" + } + ] +} diff --git a/net/adblock-fast/tests/mocks/uci/network.json b/net/adblock-fast/tests/mocks/uci/network.json new file mode 100644 index 0000000000..5b33d2007f --- /dev/null +++ b/net/adblock-fast/tests/mocks/uci/network.json @@ -0,0 +1,18 @@ +{ + "interface": [ + { + ".name": "loopback", + ".type": "interface", + "device": "lo", + "proto": "static", + "ipaddr": "127.0.0.1", + "netmask": "255.0.0.0" + }, + { + ".name": "wan", + ".type": "interface", + "device": "eth0", + "proto": "dhcp" + } + ] +} diff --git a/net/adblock-fast/tests/mocks/uci/smartdns.json b/net/adblock-fast/tests/mocks/uci/smartdns.json new file mode 100644 index 0000000000..a4885b609a --- /dev/null +++ b/net/adblock-fast/tests/mocks/uci/smartdns.json @@ -0,0 +1,9 @@ +{ + "smartdns": [ + { + ".name": "smartdns", + ".type": "smartdns", + "enabled": "0" + } + ] +} diff --git a/net/adblock-fast/tests/mocks/uci/unbound.json b/net/adblock-fast/tests/mocks/uci/unbound.json new file mode 100644 index 0000000000..46b13b58b5 --- /dev/null +++ b/net/adblock-fast/tests/mocks/uci/unbound.json @@ -0,0 +1,9 @@ +{ + "unbound": [ + { + ".name": "unbound", + ".type": "unbound", + "enabled": "0" + } + ] +} diff --git a/net/adblock-fast/tests/run_tests.sh b/net/adblock-fast/tests/run_tests.sh new file mode 100644 index 0000000000..90eb12b57b --- /dev/null +++ b/net/adblock-fast/tests/run_tests.sh @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Functional test runner for adblock-fast. +# +# Adapts the mwan4 mock-and-expect pattern for adblock-fast: +# - Patches ES module imports to require() calls +# - Redirects hardcoded paths to a temp directory +# - Exports internal functions for test access +# - Uses real shell commands (sed/sort/grep/awk) with mock UCI/UBus +# +# Usage: cd source.openwrt.melmac.ca/adblock-fast && bash tests/run_tests.sh [test_file...] + +set -o pipefail + +line='........................................' + +# ── Temp directories ───────────────────────────────────────────────── + +TESTDIR="/tmp/adb_test.$$" +patch_dir="/tmp/adb_test_modules.$$" +stub_dir="$TESTDIR/stubs" + +mkdir -p "$TESTDIR"/{var_run/adblock-fast,var,shm,var_lib_unbound,etc,cache,tmp} +mkdir -p "$patch_dir" +mkdir -p "$stub_dir" + +trap "rm -rf '$TESTDIR' '$patch_dir'" EXIT + +# ── Copy test data ─────────────────────────────────────────────────── + +cp -r ./tests/data "$TESTDIR/data" + +# ── Prepare resolved mock fixtures (replace TESTDIR placeholder) ───── + +mkdir -p "$TESTDIR/mocks_resolved/uci" "$TESTDIR/mocks_resolved/ubus" +for f in ./tests/mocks/uci/*.json; do + sed "s|TESTDIR|$TESTDIR|g" "$f" > "$TESTDIR/mocks_resolved/uci/$(basename "$f")" +done +for f in ./tests/mocks/ubus/*.json; do + cp "$f" "$TESTDIR/mocks_resolved/ubus/$(basename "$f")" +done + +# ── Create resolver stubs ─────────────────────────────────────────── + +cat > "$stub_dir/dnsmasq" << 'STUB' +#!/bin/sh +case "$1" in + --version) + echo "Dnsmasq version 2.89" + echo "Compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset nftset auth no-cryptohash no-DNSSEC loop-detect inotify dumpfile" + ;; + --test) + echo "dnsmasq: syntax check OK." + exit 0 + ;; +esac +STUB +chmod +x "$stub_dir/dnsmasq" + +for cmd in smartdns unbound; do + printf '#!/bin/sh\nexit 0\n' > "$stub_dir/$cmd" + chmod +x "$stub_dir/$cmd" +done + +# Create ipset/nft stubs +for cmd in ipset nft; do + printf '#!/bin/sh\nexit 0\n' > "$stub_dir/$cmd" + chmod +x "$stub_dir/$cmd" +done + +# Create resolveip stub +cat > "$stub_dir/resolveip" << 'STUB' +#!/bin/sh +echo "127.0.0.1" +exit 0 +STUB +chmod +x "$stub_dir/resolveip" + +# ── Patch adblock-fast.uc ─────────────────────────────────────────── + +# The sed pipeline: +# 1. Convert ES module imports to require() calls +# 2. Redirect hardcoded paths to TESTDIR +# 3. Extend is_present() search paths with stub_dir +# 4. Export internal test helpers + +sed \ + -e "s|import { readfile, writefile, popen, stat, unlink, rename, open, glob, mkdir, mkstemp, symlink, chmod, chown, realpath, lsdir, access, dirname } from 'fs';|let _fs = require('fs'), readfile = _fs.readfile, writefile = _fs.writefile, popen = _fs.popen, stat = _fs.stat, unlink = _fs.unlink, rename = _fs.rename, open = _fs.open, glob = _fs.glob, mkdir = _fs.mkdir, mkstemp = _fs.mkstemp, symlink = _fs.symlink, chmod = _fs.chmod, chown = _fs.chown, realpath = _fs.realpath, lsdir = _fs.lsdir, access = _fs.access, dirname = _fs.dirname;|" \ + -e "s|import { cursor } from 'uci';|let _uci = require('uci'), cursor = _uci.cursor;|" \ + -e "s|import { connect } from 'ubus';|let _ubus = require('ubus'), connect = _ubus.connect;|" \ + -e "s|dnsmasq_file: '/var/run/adblock-fast/adblock-fast.dnsmasq'|dnsmasq_file: '${TESTDIR}/var_run/adblock-fast/adblock-fast.dnsmasq'|" \ + -e "s|config_file: '/etc/config/adblock-fast'|config_file: '${TESTDIR}/etc/adblock-fast'|" \ + -e "s|run_file: '/dev/shm/adblock-fast'|run_file: '${TESTDIR}/shm/adblock-fast'|" \ + -e "s|status_file: '/dev/shm/adblock-fast.status.json'|status_file: '${TESTDIR}/shm/adblock-fast.status.json'|" \ + -e "s|'/var/run/' + pkg.name|'${TESTDIR}/var_run/' + pkg.name|g" \ + -e "s|'/var/lib/unbound/adb_list.' + pkg.name|'${TESTDIR}/var_run/' + pkg.name + '/adb_list.' + pkg.name|g" \ + -e "s|'/var/' + pkg.name|'${TESTDIR}/var/' + pkg.name|g" \ + -e "s|for (let dir in \['/usr/sbin', '/usr/bin', '/sbin', '/bin'\])|for (let dir in ['${stub_dir}', '/usr/sbin', '/usr/bin', '/sbin', '/bin'])|" \ + -e "s|stat('/etc/config/dhcp')|stat('${TESTDIR}/etc/dhcp')|g" \ + -e "s|stat('/etc/config/smartdns')|stat('${TESTDIR}/etc/smartdns')|g" \ + ./files/lib/adblock-fast/adblock-fast.uc > "$patch_dir/adblock-fast.uc" + +# Append test-helper exports to the patched module. +# We add a _test_internals object that gives tests access to module-private state. +# NOTE: cfg is accessed via get_cfg()/set_cfg() because env.load_config() +# reassigns cfg, which would make a direct reference stale. +sed -i '/^export default {/,/^};/{ + /process_file_url,/a\ +\t// Test helpers (injected by test runner)\ +\t_test_internals: {\ +\t\tdownload_lists: download_lists,\ +\t\tdetect_file_type: detect_file_type,\ +\t\tdns_modes: dns_modes,\ +\t\tget_cfg: function() { return cfg; },\ +\t\tset_cfg: function(k, v) { cfg[k] = v; },\ +\t\tstate: state,\ +\t\tenv: env,\ +\t\tdns_output: dns_output,\ +\t\tstatus_data: status_data,\ +\t\tlist_formats: list_formats,\ +\t\ttmp: tmp,\ +\t\tappend_urls: append_urls,\ +\t\tcount_lines: count_lines,\ +\t\tcount_blocked_domains: count_blocked_domains,\ +\t}, +}' "$patch_dir/adblock-fast.uc" + +# Patch cli.uc too (for tests that exercise the CLI path) +sed \ + -e "s|import adb from 'adblock-fast';|let adb = require('adblock-fast');|" \ + ./files/lib/adblock-fast/cli.uc > "$patch_dir/cli.uc" + +# ── Set up environment ─────────────────────────────────────────────── + +export TMPDIR="$TESTDIR/tmp" +export PATH="$stub_dir:$PATH" + +# ucode invocation: patched module first, then mocklib, then original source +ucode="ucode -S -L$patch_dir -L./tests/lib -L./files/lib/adblock-fast" + +# ── Test framework (adapted from mwan4) ────────────────────────────── + +extract_sections() { + local file=$1 + local dir=$2 + local count=0 + local tag line outfile + + while IFS= read -r line; do + case "$line" in + "-- Testcase --") + tag="test" + count=$((count + 1)) + outfile=$(printf "%s/%03d.in" "$dir" $count) + printf "" > "$outfile" + ;; + "-- Environment --") + tag="env" + count=$((count + 1)) + outfile=$(printf "%s/%03d.env" "$dir" $count) + printf "" > "$outfile" + ;; + "-- Expect stdout --"|"-- Expect stderr --"|"-- Expect exitcode --") + tag="${line#-- Expect }" + tag="${tag% --}" + count=$((count + 1)) + outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag") + printf "" > "$outfile" + ;; + "-- File "*" --") + tag="file" + outfile="${line#-- File }" + outfile="$(echo "${outfile% --}" | xargs)" + outfile="$dir/files$(readlink -m "/${outfile:-file}")" + mkdir -p "$(dirname "$outfile")" + printf "" > "$outfile" + ;; + "-- End --") + tag="" + outfile="" + ;; + *) + if [ -n "$tag" ]; then + printf "%s\\n" "$line" >> "$outfile" + fi + ;; + esac + done < "$file" + + # Post-process: replace TESTDIR placeholder in extracted files + # - files/ directory (mock data) + # - expect sections (.stdout, .stderr) so tests can reference TESTDIR paths + # NOTE: Do NOT substitute in .in files — those use TESTDIR as a ucode global variable + find "$dir/files" -type f 2>/dev/null | while read -r f; do + sed -i "s|TESTDIR|$TESTDIR|g" "$f" + done + for f in "$dir"/*.stdout "$dir"/*.stderr; do + [ -f "$f" ] && sed -i "s|TESTDIR|$TESTDIR|g" "$f" + done + + return $(ls -l "$dir/"*.in 2>/dev/null | wc -l) +} + +run_testcase() { + local num=$1 + local dir=$2 + local in=$3 + local env=$4 + local out=$5 + local err=$6 + local code=$7 + local fail=0 + + # Clean test state between runs + rm -rf "$TESTDIR"/var_run/adblock-fast/* + rm -f "$TESTDIR"/var/adblock-fast.* + rm -f "$TESTDIR"/shm/adblock-fast* + rm -f "$TESTDIR"/var_lib_unbound/* + rm -f "$TESTDIR"/tmp/adblock-fast* + mkdir -p "$TESTDIR"/var_run/adblock-fast + + $ucode \ + -D MOCK_SEARCH_PATH='["'"$dir"'/files", "'"$TESTDIR"'/mocks_resolved", "./tests/mocks"]' \ + -D TESTDIR='"'"$TESTDIR"'"' \ + ${env:+-F "$env"} \ + -l mocklib \ + - <"$in" >"$dir/res.out" 2>"$dir/res.err" + + printf "%d\n" $? > "$dir/res.code" + + touch "$dir/empty" + + if ! cmp -s "$dir/res.err" "${err:-$dir/empty}"; then + [ $fail = 0 ] && printf "!\n" + printf "Testcase #%d: Expected stderr did not match:\n" $num + diff -u --color=always --label="Expected stderr" --label="Resulting stderr" "${err:-$dir/empty}" "$dir/res.err" + printf -- "---\n" + fail=1 + fi + + if ! cmp -s "$dir/res.out" "${out:-$dir/empty}"; then + [ $fail = 0 ] && printf "!\n" + printf "Testcase #%d: Expected stdout did not match:\n" $num + diff -u --color=always --label="Expected stdout" --label="Resulting stdout" "${out:-$dir/empty}" "$dir/res.out" + printf -- "---\n" + fail=1 + fi + + if [ -n "$code" ] && ! cmp -s "$dir/res.code" "$code"; then + [ $fail = 0 ] && printf "!\n" + printf "Testcase #%d: Expected exit code did not match:\n" $num + diff -u --color=always --label="Expected code" --label="Resulting code" "$code" "$dir/res.code" + printf -- "---\n" + fail=1 + fi + + return $fail +} + +run_test() { + local file=$1 + local name=${file##*/} + local res ecode eout eerr ein eenv tests + local testcase_first=0 failed=0 count=0 + + printf "%s %s " "$name" "${line:${#name}}" + + mkdir "/tmp/test.$$" + + extract_sections "$file" "/tmp/test.$$" + tests=$? + + [ -f "/tmp/test.$$/001.in" ] && testcase_first=1 + + for res in "/tmp/test.$$/"[0-9]*; do + case "$res" in + *.in) + count=$((count + 1)) + + if [ $testcase_first = 1 ]; then + # Flush previous test + if [ -n "$ein" ]; then + run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1)) + + eout="" + eerr="" + ecode="" + eenv="" + fi + + ein=$res + else + run_testcase $count "/tmp/test.$$" "$res" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1)) + + eout="" + eerr="" + ecode="" + eenv="" + fi + + ;; + *.env) eenv=$res ;; + *.stdout) eout=$res ;; + *.stderr) eerr=$res ;; + *.exitcode) ecode=$res ;; + esac + done + + # Flush last test + if [ $testcase_first = 1 ] && [ -n "$ein" ]; then + run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1)) + fi + + rm -r "/tmp/test.$$" + + if [ $failed = 0 ]; then + printf "OK\n" + else + printf "%s %s FAILED (%d/%d)\n" "$name" "${line:${#name}}" $failed $tests + fi + + return $failed +} + + +n_tests=0 +n_fails=0 + +select_tests="$@" + +use_test() { + local input="$(readlink -f "$1")" + local test + + [ -f "$input" ] || return 1 + [ -n "$select_tests" ] || return 0 + + for test in $select_tests; do + test="$(readlink -f "$test")" + + [ "$test" != "$input" ] || return 0 + done + + return 1 +} + +for catdir in tests/[0-9][0-9]_*; do + [ -d "$catdir" ] || continue + + printf "\n##\n## Running %s tests\n##\n\n" "${catdir##*/[0-9][0-9]_}" + + for testfile in "$catdir/"[0-9][0-9]_*; do + use_test "$testfile" || continue + + n_tests=$((n_tests + 1)) + run_test "$testfile" || n_fails=$((n_fails + 1)) + done +done + +# ── Shell script syntax checks ────────────────────────────────────── + +printf "\n##\n## Checking shell script syntax\n##\n\n" +for shellscript in \ + files/etc/init.d/* \ + files/etc/uci-defaults/*; do + [ -f "$shellscript" ] || continue + head -1 "$shellscript" | grep -q '^#!/bin/sh' || continue + name="${shellscript#files/}" + n_tests=$((n_tests + 1)) + printf "%s %s " "$name" "${line:${#name}}" + if sh -n "$shellscript" 2>/dev/null; then + printf "OK\n" + else + printf "FAIL\n" + sh -n "$shellscript" + n_fails=$((n_fails + 1)) + fi +done + +printf "\nRan %d tests, %d okay, %d failures\n" $n_tests $((n_tests - n_fails)) $n_fails +exit $n_fails