fdroidserver-1.1.6/0000755000175000017500000000000013576156553014141 5ustar hanshans00000000000000fdroidserver-1.1.6/CHANGELOG.md0000644000175000017500000000227513576156546015762 0ustar hanshans00000000000000 ### 1.1.4 (2019-08-15) * include bitcoin validation regex required by fdroiddata * merged Debian patches to fix test suite there ### 1.1.3 (2019-07-03) * fixed test suite when run from source tarball * fixed test runs in Debian ### 1.1.2 (2019-03-29) * fix bug while downloading repo index ([!636](https://gitlab.com/fdroid/fdroidserver/merge_requests/636)) ### 1.1.1 (2019-02-03) * support APK Signature v2 and v3 * all SDK Version values are output as integers in the index JSON * take graphics from Fastlane dirs using any valid RFC5646 locale * print warning if not running in UTF-8 encoding * fdroid build: hide --on-server cli flag ### 1.1 (2019-01-28) * a huge update with many fixes and new features: https://gitlab.com/fdroid/fdroidserver/milestones/7 * can run without and Android SDK installed * much more reliable operation with large binary APK collections * sync all translations, including newly added languages: hu it ko pl pt_PT ru * many security fixes, based on the security audit * NoSourceSince automatically adds SourceGone Anti-Feature * aapt scraping works with all known aapt versions * smoother mirror setups * much faster `fdroid update` when using androguard fdroidserver-1.1.6/LICENSE0000644000175000017500000010333013576156531015142 0ustar hanshans00000000000000 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 . fdroidserver-1.1.6/MANIFEST.in0000644000175000017500000013702613576156531015704 0ustar hanshans00000000000000include buildserver/config.buildserver.py include buildserver/provision-android-ndk include buildserver/provision-android-sdk include buildserver/provision-apt-get-install include buildserver/provision-apt-proxy include buildserver/provision-gradle include buildserver/setup-env-vars include buildserver/Vagrantfile include CHANGELOG.md include completion/bash-completion include docker/Dockerfile include docker/drozer.py include docker/enable_service.py include docker/entrypoint.sh include docker/install_agent.py include docker/Makefile include docker/README.md include examples/config.py include examples/fdroid-icon.png include examples/makebuildserver.config.py include examples/opensc-fdroid.cfg include examples/public-read-only-s3-bucket-policy.json include examples/template.yml include fdroid include gradlew-fdroid include LICENSE include locale/bo/LC_MESSAGES/fdroidserver.mo include locale/de/LC_MESSAGES/fdroidserver.mo include locale/es/LC_MESSAGES/fdroidserver.mo include locale/fr/LC_MESSAGES/fdroidserver.mo include locale/hu/LC_MESSAGES/fdroidserver.mo include locale/it/LC_MESSAGES/fdroidserver.mo include locale/ko/LC_MESSAGES/fdroidserver.mo include locale/nb_NO/LC_MESSAGES/fdroidserver.mo include locale/pl/LC_MESSAGES/fdroidserver.mo include locale/pt_BR/LC_MESSAGES/fdroidserver.mo include locale/pt_PT/LC_MESSAGES/fdroidserver.mo include locale/ru/LC_MESSAGES/fdroidserver.mo include locale/tr/LC_MESSAGES/fdroidserver.mo include locale/uk/LC_MESSAGES/fdroidserver.mo include locale/zh_Hans/LC_MESSAGES/fdroidserver.mo include locale/zh_Hant/LC_MESSAGES/fdroidserver.mo include makebuildserver include README.md include tests/androguard_test.py include tests/bad-unicode-*.apk include tests/build.TestCase include tests/build-tools/17.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/17.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/17.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/17.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/17.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/17.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/17.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/17.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/17.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/17.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/17.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/17.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/17.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/17.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/17.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/18.1.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/18.1.1/aapt-output-com.politedroid_3.txt include tests/build-tools/18.1.1/aapt-output-com.politedroid_4.txt include tests/build-tools/18.1.1/aapt-output-com.politedroid_5.txt include tests/build-tools/18.1.1/aapt-output-com.politedroid_6.txt include tests/build-tools/18.1.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/18.1.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/18.1.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/18.1.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/18.1.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/18.1.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/18.1.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/18.1.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/18.1.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/18.1.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/19.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/19.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/19.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/19.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/19.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/19.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/19.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/19.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/19.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/19.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/19.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/19.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/19.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/19.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/19.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/19.1.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/19.1.0/aapt-output-com.politedroid_3.txt include tests/build-tools/19.1.0/aapt-output-com.politedroid_4.txt include tests/build-tools/19.1.0/aapt-output-com.politedroid_5.txt include tests/build-tools/19.1.0/aapt-output-com.politedroid_6.txt include tests/build-tools/19.1.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/19.1.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/19.1.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/19.1.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/19.1.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/19.1.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/19.1.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/19.1.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/19.1.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/19.1.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/20.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/20.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/20.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/20.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/20.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/20.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/20.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/20.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/20.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/20.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/20.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/20.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/20.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/20.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/20.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/21.1.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/21.1.1/aapt-output-com.politedroid_3.txt include tests/build-tools/21.1.1/aapt-output-com.politedroid_4.txt include tests/build-tools/21.1.1/aapt-output-com.politedroid_5.txt include tests/build-tools/21.1.1/aapt-output-com.politedroid_6.txt include tests/build-tools/21.1.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/21.1.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/21.1.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/21.1.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/21.1.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/21.1.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/21.1.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/21.1.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/21.1.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/21.1.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/21.1.2/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/21.1.2/aapt-output-com.politedroid_3.txt include tests/build-tools/21.1.2/aapt-output-com.politedroid_4.txt include tests/build-tools/21.1.2/aapt-output-com.politedroid_5.txt include tests/build-tools/21.1.2/aapt-output-com.politedroid_6.txt include tests/build-tools/21.1.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/21.1.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/21.1.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/21.1.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/21.1.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/21.1.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/21.1.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/21.1.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/21.1.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/21.1.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/22.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/22.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/22.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/22.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/22.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/22.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/22.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/22.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/22.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/22.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/22.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/22.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/22.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/22.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/22.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/22.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/22.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/22.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/22.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/22.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/22.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/22.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/22.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/22.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/22.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/22.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/22.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/22.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/22.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/22.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/23.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/23.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/23.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/23.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/23.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/23.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/23.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/23.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/23.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/23.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/23.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/23.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/23.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/23.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/23.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/23.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/23.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/23.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/23.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/23.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/23.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/23.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/23.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/23.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/23.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/23.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/23.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/23.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/23.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/23.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/23.0.2/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/23.0.2/aapt-output-com.politedroid_3.txt include tests/build-tools/23.0.2/aapt-output-com.politedroid_4.txt include tests/build-tools/23.0.2/aapt-output-com.politedroid_5.txt include tests/build-tools/23.0.2/aapt-output-com.politedroid_6.txt include tests/build-tools/23.0.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/23.0.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/23.0.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/23.0.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/23.0.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/23.0.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/23.0.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/23.0.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/23.0.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/23.0.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/23.0.3/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/23.0.3/aapt-output-com.politedroid_3.txt include tests/build-tools/23.0.3/aapt-output-com.politedroid_4.txt include tests/build-tools/23.0.3/aapt-output-com.politedroid_5.txt include tests/build-tools/23.0.3/aapt-output-com.politedroid_6.txt include tests/build-tools/23.0.3/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/23.0.3/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/23.0.3/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/23.0.3/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/23.0.3/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/23.0.3/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/23.0.3/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/23.0.3/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/23.0.3/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/23.0.3/aapt-output-souch.smsbypass_9.txt include tests/build-tools/24.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/24.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/24.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/24.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/24.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/24.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/24.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/24.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/24.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/24.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/24.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/24.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/24.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/24.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/24.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/24.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/24.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/24.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/24.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/24.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/24.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/24.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/24.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/24.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/24.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/24.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/24.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/24.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/24.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/24.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/24.0.2/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/24.0.2/aapt-output-com.politedroid_3.txt include tests/build-tools/24.0.2/aapt-output-com.politedroid_4.txt include tests/build-tools/24.0.2/aapt-output-com.politedroid_5.txt include tests/build-tools/24.0.2/aapt-output-com.politedroid_6.txt include tests/build-tools/24.0.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/24.0.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/24.0.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/24.0.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/24.0.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/24.0.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/24.0.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/24.0.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/24.0.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/24.0.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/24.0.3/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/24.0.3/aapt-output-com.politedroid_3.txt include tests/build-tools/24.0.3/aapt-output-com.politedroid_4.txt include tests/build-tools/24.0.3/aapt-output-com.politedroid_5.txt include tests/build-tools/24.0.3/aapt-output-com.politedroid_6.txt include tests/build-tools/24.0.3/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/24.0.3/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/24.0.3/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/24.0.3/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/24.0.3/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/24.0.3/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/24.0.3/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/24.0.3/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/24.0.3/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/24.0.3/aapt-output-souch.smsbypass_9.txt include tests/build-tools/25.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/25.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/25.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/25.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/25.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/25.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/25.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/25.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/25.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/25.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/25.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/25.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/25.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/25.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/25.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/25.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/25.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/25.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/25.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/25.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/25.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/25.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/25.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/25.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/25.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/25.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/25.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/25.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/25.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/25.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/25.0.2/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/25.0.2/aapt-output-com.politedroid_3.txt include tests/build-tools/25.0.2/aapt-output-com.politedroid_4.txt include tests/build-tools/25.0.2/aapt-output-com.politedroid_5.txt include tests/build-tools/25.0.2/aapt-output-com.politedroid_6.txt include tests/build-tools/25.0.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/25.0.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/25.0.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/25.0.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/25.0.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/25.0.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/25.0.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/25.0.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/25.0.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/25.0.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/25.0.3/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/25.0.3/aapt-output-com.politedroid_3.txt include tests/build-tools/25.0.3/aapt-output-com.politedroid_4.txt include tests/build-tools/25.0.3/aapt-output-com.politedroid_5.txt include tests/build-tools/25.0.3/aapt-output-com.politedroid_6.txt include tests/build-tools/25.0.3/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/25.0.3/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/25.0.3/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/25.0.3/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/25.0.3/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/25.0.3/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/25.0.3/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/25.0.3/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/25.0.3/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/25.0.3/aapt-output-souch.smsbypass_9.txt include tests/build-tools/26.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/26.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/26.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/26.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/26.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/26.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/26.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/26.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/26.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/26.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/26.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/26.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/26.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/26.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/26.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/26.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/26.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/26.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/26.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/26.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/26.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/26.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/26.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/26.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/26.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/26.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/26.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/26.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/26.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/26.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/26.0.2/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/26.0.2/aapt-output-com.politedroid_3.txt include tests/build-tools/26.0.2/aapt-output-com.politedroid_4.txt include tests/build-tools/26.0.2/aapt-output-com.politedroid_5.txt include tests/build-tools/26.0.2/aapt-output-com.politedroid_6.txt include tests/build-tools/26.0.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/26.0.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/26.0.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/26.0.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/26.0.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/26.0.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/26.0.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/26.0.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/26.0.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/26.0.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/26.0.3/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/26.0.3/aapt-output-com.politedroid_3.txt include tests/build-tools/26.0.3/aapt-output-com.politedroid_4.txt include tests/build-tools/26.0.3/aapt-output-com.politedroid_5.txt include tests/build-tools/26.0.3/aapt-output-com.politedroid_6.txt include tests/build-tools/26.0.3/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/26.0.3/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/26.0.3/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/26.0.3/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/26.0.3/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/26.0.3/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/26.0.3/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/26.0.3/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/26.0.3/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/26.0.3/aapt-output-souch.smsbypass_9.txt include tests/build-tools/27.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/27.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/27.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/27.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/27.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/27.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/27.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/27.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/27.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/27.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/27.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/27.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/27.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/27.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/27.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/27.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/27.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/27.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/27.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/27.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/27.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/27.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/27.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/27.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/27.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/27.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/27.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/27.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/27.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/27.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/27.0.2/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/27.0.2/aapt-output-com.politedroid_3.txt include tests/build-tools/27.0.2/aapt-output-com.politedroid_4.txt include tests/build-tools/27.0.2/aapt-output-com.politedroid_5.txt include tests/build-tools/27.0.2/aapt-output-com.politedroid_6.txt include tests/build-tools/27.0.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/27.0.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/27.0.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/27.0.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/27.0.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/27.0.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/27.0.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/27.0.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/27.0.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/27.0.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/27.0.3/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/27.0.3/aapt-output-com.politedroid_3.txt include tests/build-tools/27.0.3/aapt-output-com.politedroid_4.txt include tests/build-tools/27.0.3/aapt-output-com.politedroid_5.txt include tests/build-tools/27.0.3/aapt-output-com.politedroid_6.txt include tests/build-tools/27.0.3/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/27.0.3/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/27.0.3/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/27.0.3/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/27.0.3/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/27.0.3/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/27.0.3/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/27.0.3/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/27.0.3/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/27.0.3/aapt-output-souch.smsbypass_9.txt include tests/build-tools/28.0.0/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/28.0.0/aapt-output-com.politedroid_3.txt include tests/build-tools/28.0.0/aapt-output-com.politedroid_4.txt include tests/build-tools/28.0.0/aapt-output-com.politedroid_5.txt include tests/build-tools/28.0.0/aapt-output-com.politedroid_6.txt include tests/build-tools/28.0.0/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/28.0.0/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/28.0.0/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/28.0.0/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/28.0.0/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/28.0.0/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/28.0.0/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/28.0.0/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/28.0.0/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/28.0.0/aapt-output-souch.smsbypass_9.txt include tests/build-tools/28.0.1/aapt-output-com.moez.QKSMS_182.txt include tests/build-tools/28.0.1/aapt-output-com.politedroid_3.txt include tests/build-tools/28.0.1/aapt-output-com.politedroid_4.txt include tests/build-tools/28.0.1/aapt-output-com.politedroid_5.txt include tests/build-tools/28.0.1/aapt-output-com.politedroid_6.txt include tests/build-tools/28.0.1/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/28.0.1/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/28.0.1/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/28.0.1/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/28.0.1/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/28.0.1/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/28.0.1/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/28.0.1/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/28.0.1/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/28.0.1/aapt-output-souch.smsbypass_9.txt include tests/build-tools/28.0.2/aapt-output-com.politedroid_3.txt include tests/build-tools/28.0.2/aapt-output-com.politedroid_4.txt include tests/build-tools/28.0.2/aapt-output-com.politedroid_5.txt include tests/build-tools/28.0.2/aapt-output-com.politedroid_6.txt include tests/build-tools/28.0.2/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/28.0.2/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/28.0.2/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/28.0.2/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/28.0.2/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/28.0.2/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/28.0.2/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/28.0.2/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/28.0.2/aapt-output-org.droidtr.keyboard_34.txt include tests/build-tools/28.0.2/aapt-output-souch.smsbypass_9.txt include tests/build-tools/28.0.3/aapt-output-com.example.test.helloworld_1.txt include tests/build-tools/28.0.3/aapt-output-com.politedroid_3.txt include tests/build-tools/28.0.3/aapt-output-com.politedroid_4.txt include tests/build-tools/28.0.3/aapt-output-com.politedroid_5.txt include tests/build-tools/28.0.3/aapt-output-com.politedroid_6.txt include tests/build-tools/28.0.3/aapt-output-duplicate.permisssions_9999999.txt include tests/build-tools/28.0.3/aapt-output-info.guardianproject.urzip_100.txt include tests/build-tools/28.0.3/aapt-output-info.zwanenburg.caffeinetile_4.txt include tests/build-tools/28.0.3/aapt-output-no.min.target.sdk_987.txt include tests/build-tools/28.0.3/aapt-output-obb.main.oldversion_1444412523.txt include tests/build-tools/28.0.3/aapt-output-obb.mainpatch.current_1619.txt include tests/build-tools/28.0.3/aapt-output-obb.main.twoversions_1101613.txt include tests/build-tools/28.0.3/aapt-output-obb.main.twoversions_1101615.txt include tests/build-tools/28.0.3/aapt-output-obb.main.twoversions_1101617.txt include tests/build-tools/28.0.3/aapt-output-souch.smsbypass_9.txt include tests/build-tools/generate.sh include tests/check-fdroid-apk include tests/common.TestCase include tests/complete-ci-tests include tests/config.py include tests/description-parsing.py include tests/dummy-keystore.jks include tests/dump_internal_metadata_format.py include tests/exception.TestCase include tests/extra/convert_metadata_to_yaml_then_txt.sh include tests/extra/manual-vmtools-test.py include tests/getsig/getsig.java include tests/getsig/make.sh include tests/getsig/run.sh include tests/gnupghome/pubring.gpg include tests/gnupghome/random_seed include tests/gnupghome/secring.gpg include tests/gnupghome/trustdb.gpg include tests/import_proxy.py include tests/import.TestCase include tests/index.TestCase include tests/install.TestCase include tests/IsMD5Disabled.java include tests/janus.apk include tests/keystore.jks include tests/lint.TestCase include tests/metadata/apk/info.guardianproject.urzip.yaml include tests/metadata/apk/org.dyndns.fules.ck.yaml include tests/metadata/app.with.special.build.params.txt include tests/metadata/com.politedroid.txt include tests/metadata/dump/com.politedroid.yaml include tests/metadata/dump/org.adaway.yaml include tests/metadata/dump/org.smssecure.smssecure.yaml include tests/metadata/dump/org.videolan.vlc.yaml include tests/metadata/duplicate.permisssions.yml include tests/metadata/fake.ota.update.txt include tests/metadata/info.guardianproject.checkey/en-US/description.txt include tests/metadata/info.guardianproject.checkey/en-US/phoneScreenshots/checkey-phone.png include tests/metadata/info.guardianproject.checkey/en-US/phoneScreenshots/checkey.png include tests/metadata/info.guardianproject.checkey/en-US/summary.txt include tests/metadata/info.guardianproject.checkey.txt include tests/metadata/info.guardianproject.urzip/en-US/changelogs/100.txt include tests/metadata/info.guardianproject.urzip/en-US/full_description.txt include tests/metadata/info.guardianproject.urzip/en-US/images/featureGraphic.png include tests/metadata/info.guardianproject.urzip/en-US/images/icon.png include tests/metadata/info.guardianproject.urzip/en-US/short_description.txt include tests/metadata/info.guardianproject.urzip/en-US/title.txt include tests/metadata/info.guardianproject.urzip/en-US/video.txt include tests/metadata/info.guardianproject.urzip.yml include tests/metadata/info.zwanenburg.caffeinetile.yml include tests/metadata/no.min.target.sdk.yml include tests/metadata/obb.main.oldversion.txt include tests/metadata/obb.mainpatch.current.txt include tests/metadata/obb.main.twoversions.txt include tests/metadata/org.adaway.json include tests/metadata/org.fdroid.ci.test.app.txt include tests/metadata/org.fdroid.fdroid.txt include tests/metadata/org.smssecure.smssecure/signatures/134/28969C09.RSA include tests/metadata/org.smssecure.smssecure/signatures/134/28969C09.SF include tests/metadata/org.smssecure.smssecure/signatures/134/MANIFEST.MF include tests/metadata/org.smssecure.smssecure/signatures/135/28969C09.RSA include tests/metadata/org.smssecure.smssecure/signatures/135/28969C09.SF include tests/metadata/org.smssecure.smssecure/signatures/135/MANIFEST.MF include tests/metadata/org.smssecure.smssecure.txt include tests/metadata/org.videolan.vlc.yml include tests/metadata/raw.template.txt include tests/metadata-rewrite-yml/app.with.special.build.params.yml include tests/metadata-rewrite-yml/fake.ota.update.yml include tests/metadata-rewrite-yml/org.fdroid.fdroid.yml include tests/metadata/souch.smsbypass.txt include tests/metadata.TestCase include tests/openssl-version-check-test.py include tests/org.bitbucket.tickytacky.mirrormirror_1.apk include tests/org.bitbucket.tickytacky.mirrormirror_2.apk include tests/org.bitbucket.tickytacky.mirrormirror_3.apk include tests/org.bitbucket.tickytacky.mirrormirror_4.apk include tests/org.dyndns.fules.ck_20.apk include tests/publish.TestCase include tests/repo/categories.txt include tests/repo/com.example.test.helloworld_1.apk include tests/repo/com.politedroid_3.apk include tests/repo/com.politedroid_4.apk include tests/repo/com.politedroid_5.apk include tests/repo/com.politedroid_6.apk include tests/repo/duplicate.permisssions_9999999.apk include tests/repo/fake.ota.update_1234.zip include tests/repo/index-v1.json include tests/repo/index.xml include tests/repo/info.zwanenburg.caffeinetile_4.apk include tests/repo/main.1101613.obb.main.twoversions.obb include tests/repo/main.1101615.obb.main.twoversions.obb include tests/repo/main.1434483388.obb.main.oldversion.obb include tests/repo/main.1619.obb.mainpatch.current.obb include tests/repo/no.min.target.sdk_987.apk include tests/repo/obb.main.oldversion_1444412523.apk include tests/repo/obb.mainpatch.current_1619_another-release-key.apk include tests/repo/obb.mainpatch.current_1619.apk include tests/repo/obb.mainpatch.current/en-US/featureGraphic.png include tests/repo/obb.mainpatch.current/en-US/icon.png include tests/repo/obb.mainpatch.current/en-US/phoneScreenshots/screenshot-main.png include tests/repo/obb.mainpatch.current/en-US/sevenInchScreenshots/screenshot-tablet-main.png include tests/repo/obb.main.twoversions_1101613.apk include tests/repo/obb.main.twoversions_1101615.apk include tests/repo/obb.main.twoversions_1101617.apk include tests/repo/obb.main.twoversions_1101617_src.tar.gz include tests/repo/org.videolan.vlc/en-US/icon.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot10.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot12.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot15.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot18.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot20.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot22.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot4.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot7.png include tests/repo/org.videolan.vlc/en-US/phoneScreenshots/screenshot9.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot0.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot11.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot13.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot14.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot16.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot17.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot19.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot1.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot21.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot23.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot2.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot3.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot5.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot6.png include tests/repo/org.videolan.vlc/en-US/sevenInchScreenshots/screenshot8.png include tests/repo/patch.1619.obb.mainpatch.current.obb include tests/repo/souch.smsbypass_9.apk include tests/repo/urzip-*.apk include tests/repo/v1.v2.sig_1020.apk include tests/run-tests include tests/scanner.TestCase include tests/server.TestCase include tests/signatures.TestCase include tests/signindex/guardianproject.jar include tests/signindex/guardianproject-v1.jar include tests/signindex/testy.jar include tests/signindex/unsigned.jar include tests/source-files/at.bitfire.davdroid/build.gradle include tests/source-files/com.kunzisoft.testcase/build.gradle include tests/source-files/com.nextcloud.client/build.gradle include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/full_description.txt include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/short_description.txt include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/title.txt include tests/source-files/com.nextcloud.client.dev/src/versionDev/fastlane/metadata/android/en-US/full_description.txt include tests/source-files/com.nextcloud.client.dev/src/versionDev/fastlane/metadata/android/en-US/short_description.txt include tests/source-files/com.nextcloud.client.dev/src/versionDev/fastlane/metadata/android/en-US/title.txt include tests/source-files/com.nextcloud.client/src/generic/fastlane/metadata/android/en-US/full_description.txt include tests/source-files/com.nextcloud.client/src/generic/fastlane/metadata/android/en-US/short_description.txt include tests/source-files/com.nextcloud.client/src/generic/fastlane/metadata/android/en-US/title.txt include tests/source-files/com.nextcloud.client/src/versionDev/fastlane/metadata/android/en-US/full_description.txt include tests/source-files/com.nextcloud.client/src/versionDev/fastlane/metadata/android/en-US/short_description.txt include tests/source-files/com.nextcloud.client/src/versionDev/fastlane/metadata/android/en-US/title.txt include tests/source-files/eu.siacs.conversations/build.gradle include tests/source-files/eu.siacs.conversations/metadata/en-US/name.txt include tests/source-files/fdroid/fdroidclient/AndroidManifest.xml include tests/source-files/fdroid/fdroidclient/build.gradle include tests/source-files/firebase-suspect/app/build.gradle include tests/source-files/firebase-suspect/build.gradle include tests/source-files/firebase-whitelisted/app/build.gradle include tests/source-files/firebase-whitelisted/build.gradle include tests/source-files/open-keychain/open-keychain/build.gradle include tests/source-files/open-keychain/open-keychain/OpenKeychain/build.gradle include tests/source-files/osmandapp/osmand/build.gradle include tests/source-files/Zillode/syncthing-silk/build.gradle include tests/SpeedoMeterApp.main_1.apk include tests/stats/known_apks.txt include tests/testcommon.py include tests/update.TestCase include tests/urzip.apk include tests/urzip-badcert.apk include tests/urzip-badsig.apk include tests/urzip-release.apk include tests/urzip-release-unsigned.apk include tests/v2.only.sig_2.apk include tests/valid-package-names/random-package-names include tests/valid-package-names/RandomPackageNames.java include tests/valid-package-names/test.py fdroidserver-1.1.6/PKG-INFO0000644000175000017500000001475513576156553015252 0ustar hanshans00000000000000Metadata-Version: 2.1 Name: fdroidserver Version: 1.1.6 Summary: F-Droid Server Tools Home-page: https://f-droid.org Author: The F-Droid Project Author-email: team@f-droid.org License: AGPL-3.0 Description: | CI Builds | fdroidserver | buildserver | fdroid build --all | publishing tools | |--------------------------|:-------------:|:-----------:|:------------------:|:----------------:| | Debian | [![fdroidserver status on Debian](https://gitlab.com/fdroid/fdroidserver/badges/master/build.svg)](https://gitlab.com/fdroid/fdroidserver/builds) | [![buildserver status](https://jenkins.debian.net/job/reproducible_setup_fdroid_build_environment/badge/icon)](https://jenkins.debian.net/job/reproducible_setup_fdroid_build_environment) | [![fdroid build all status](https://jenkins.debian.net/job/reproducible_fdroid_build_apps/badge/icon)](https://jenkins.debian.net/job/reproducible_fdroid_build_apps/) | [![fdroid test status](https://jenkins.debian.net/job/reproducible_fdroid_test/badge/icon)](https://jenkins.debian.net/job/reproducible_fdroid_test/) | | macOS & Ubuntu/trusty | [![fdroidserver status on macOS & Ubuntu/LTS](https://travis-ci.org/fdroidtravis/fdroidserver.svg?branch=master)](https://travis-ci.org/fdroidtravis/fdroidserver) | | | | # F-Droid Server Server for [F-Droid](https://f-droid.org), the Free Software repository system for Android. The F-Droid server tools provide various scripts and tools that are used to maintain the main [F-Droid application repository](https://f-droid.org/packages). You can use these same tools to create your own additional or alternative repository for publishing, or to assist in creating, testing and submitting metadata to the main repository. For documentation, please see , or you can find the source for the documentation in [fdroid/fdroid-website](https://gitlab.com/fdroid/fdroid-website). ### What is F-Droid? F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device. ### Installing There are many ways to install _fdroidserver_, they are documented on the website: https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools All sorts of other documentation lives there as well. ### Tests There are many components to all of the tests for the components in this git repo. The most commonly used parts of well tested, while some parts still lack tests. This test suite has built over time a bit haphazardly, so it is not as clean, organized, or complete as it could be. We welcome contributions. Before rearchitecting any parts of it, be sure to [contact us](https://f-droid.org/about) to discuss the changes beforehand. #### `fdroid` commands The test suite for all of the `fdroid` commands is in the _tests/_ subdir. _.gitlab-ci.yml_ and _.travis.yml_ run this test suite on various configurations. * _tests/complete-ci-tests_ runs _pylint_ and all tests on two different pyvenvs * _tests/run-tests_ runs the whole test suite * _tests/*.TestCase_ are individual unit tests for all of the `fdroid` commands, which can be run separately, e.g. `./update.TestCase`. #### Additional tests for different linux distributions These tests are also run on various distributions through GitLab CI. This is only enabled for `master@fdroid/fdroidserver` because it'll take longer to complete than the regular CI tests. Most of the time you won't need to worry about them but sometimes it might make sense to also run them for your merge request. In that case you need to remove [these lines from .gitlab-ci.yml](https://gitlab.com/fdroid/fdroidserver/blob/master/.gitlab-ci.yml#L34-35) and push this to a new branch of your fork. Alternatively [run them locally](https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-exec) like this: `gitlab-runner exec docker ubuntu_lts` #### buildserver The tests for the whole build server setup are entirely separate because they require at least 200GB of disk space, and 8GB of RAM. These test scripts are in the root of the project, all starting with _jenkins-_ since they are run on https://jenkins.debian.net. ### Drozer Scanner There is a new feature under development that can scan any APK in a repo, or any build, using Drozer. Drozer is a dynamic exploit scanner, it runs an app in the emulator and runs known exploits on it. This setup requires specific versions of two Python modules: _docker-py_ 1.9.0 and _requests_ older than 2.11. Other versions might cause the docker-py connection to break with the containers. Newer versions of docker-py might have this fixed already. For Debian based distributions: apt-get install libffi-dev libssl-dev python-docker ## Translation Everything can be translated. See [Translation and Localization](https://f-droid.org/docs/Translation_and_Localization) for more info. [![translation status](https://hosted.weblate.org/widgets/f-droid/-/fdroidserver/multi-auto.svg)](https://hosted.weblate.org/engage/f-droid/?utm_source=widget) Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: Intended Audience :: Telecommunications Industry Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Unix Classifier: Topic :: Utilities Requires-Python: >=3.4 Description-Content-Type: text/markdown fdroidserver-1.1.6/README.md0000644000175000017500000001154713576156546015432 0ustar hanshans00000000000000 | CI Builds | fdroidserver | buildserver | fdroid build --all | publishing tools | |--------------------------|:-------------:|:-----------:|:------------------:|:----------------:| | Debian | [![fdroidserver status on Debian](https://gitlab.com/fdroid/fdroidserver/badges/master/build.svg)](https://gitlab.com/fdroid/fdroidserver/builds) | [![buildserver status](https://jenkins.debian.net/job/reproducible_setup_fdroid_build_environment/badge/icon)](https://jenkins.debian.net/job/reproducible_setup_fdroid_build_environment) | [![fdroid build all status](https://jenkins.debian.net/job/reproducible_fdroid_build_apps/badge/icon)](https://jenkins.debian.net/job/reproducible_fdroid_build_apps/) | [![fdroid test status](https://jenkins.debian.net/job/reproducible_fdroid_test/badge/icon)](https://jenkins.debian.net/job/reproducible_fdroid_test/) | | macOS & Ubuntu/trusty | [![fdroidserver status on macOS & Ubuntu/LTS](https://travis-ci.org/fdroidtravis/fdroidserver.svg?branch=master)](https://travis-ci.org/fdroidtravis/fdroidserver) | | | | # F-Droid Server Server for [F-Droid](https://f-droid.org), the Free Software repository system for Android. The F-Droid server tools provide various scripts and tools that are used to maintain the main [F-Droid application repository](https://f-droid.org/packages). You can use these same tools to create your own additional or alternative repository for publishing, or to assist in creating, testing and submitting metadata to the main repository. For documentation, please see , or you can find the source for the documentation in [fdroid/fdroid-website](https://gitlab.com/fdroid/fdroid-website). ### What is F-Droid? F-Droid is an installable catalogue of FOSS (Free and Open Source Software) applications for the Android platform. The client makes it easy to browse, install, and keep track of updates on your device. ### Installing There are many ways to install _fdroidserver_, they are documented on the website: https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools All sorts of other documentation lives there as well. ### Tests There are many components to all of the tests for the components in this git repo. The most commonly used parts of well tested, while some parts still lack tests. This test suite has built over time a bit haphazardly, so it is not as clean, organized, or complete as it could be. We welcome contributions. Before rearchitecting any parts of it, be sure to [contact us](https://f-droid.org/about) to discuss the changes beforehand. #### `fdroid` commands The test suite for all of the `fdroid` commands is in the _tests/_ subdir. _.gitlab-ci.yml_ and _.travis.yml_ run this test suite on various configurations. * _tests/complete-ci-tests_ runs _pylint_ and all tests on two different pyvenvs * _tests/run-tests_ runs the whole test suite * _tests/*.TestCase_ are individual unit tests for all of the `fdroid` commands, which can be run separately, e.g. `./update.TestCase`. #### Additional tests for different linux distributions These tests are also run on various distributions through GitLab CI. This is only enabled for `master@fdroid/fdroidserver` because it'll take longer to complete than the regular CI tests. Most of the time you won't need to worry about them but sometimes it might make sense to also run them for your merge request. In that case you need to remove [these lines from .gitlab-ci.yml](https://gitlab.com/fdroid/fdroidserver/blob/master/.gitlab-ci.yml#L34-35) and push this to a new branch of your fork. Alternatively [run them locally](https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-exec) like this: `gitlab-runner exec docker ubuntu_lts` #### buildserver The tests for the whole build server setup are entirely separate because they require at least 200GB of disk space, and 8GB of RAM. These test scripts are in the root of the project, all starting with _jenkins-_ since they are run on https://jenkins.debian.net. ### Drozer Scanner There is a new feature under development that can scan any APK in a repo, or any build, using Drozer. Drozer is a dynamic exploit scanner, it runs an app in the emulator and runs known exploits on it. This setup requires specific versions of two Python modules: _docker-py_ 1.9.0 and _requests_ older than 2.11. Other versions might cause the docker-py connection to break with the containers. Newer versions of docker-py might have this fixed already. For Debian based distributions: apt-get install libffi-dev libssl-dev python-docker ## Translation Everything can be translated. See [Translation and Localization](https://f-droid.org/docs/Translation_and_Localization) for more info. [![translation status](https://hosted.weblate.org/widgets/f-droid/-/fdroidserver/multi-auto.svg)](https://hosted.weblate.org/engage/f-droid/?utm_source=widget) fdroidserver-1.1.6/buildserver/0000755000175000017500000000000013576156553016467 5ustar hanshans00000000000000fdroidserver-1.1.6/buildserver/Vagrantfile0000644000175000017500000000643513576156531020660 0ustar hanshans00000000000000 require 'yaml' require 'pathname' srvpath = Pathname.new(File.dirname(__FILE__)).realpath configfile = YAML.load_file(File.join(srvpath, "/Vagrantfile.yaml")) Vagrant.configure("2") do |config| # these two caching methods conflict, so only use one at a time if Vagrant.has_plugin?("vagrant-cachier") and not configfile.has_key? "aptcachedir" config.cache.scope = :box config.cache.auto_detect = false config.cache.enable :apt config.cache.enable :chef end config.vm.box = configfile['basebox'] if configfile.has_key? "basebox_version" config.vm.box_version = configfile['basebox_version'] end if not configfile.has_key? "vm_provider" or configfile["vm_provider"] == "virtualbox" # default to VirtualBox if not set config.vm.provider "virtualbox" do |v| v.customize ["modifyvm", :id, "--memory", configfile['memory']] v.customize ["modifyvm", :id, "--cpus", configfile['cpus']] v.customize ["modifyvm", :id, "--hwvirtex", configfile['hwvirtex']] end synced_folder_type = 'virtualbox' elsif configfile["vm_provider"] == "libvirt" # use KVM/QEMU if this is running in KVM/QEMU config.vm.provider :libvirt do |libvirt| libvirt.driver = configfile["hwvirtex"] == "on" ? "kvm" : "qemu" libvirt.host = "localhost" libvirt.uri = "qemu:///system" libvirt.cpus = configfile["cpus"] libvirt.memory = configfile["memory"] if configfile.has_key? "libvirt_disk_bus" libvirt.disk_bus = configfile["libvirt_disk_bus"] end if configfile.has_key? "libvirt_nic_model_type" libvirt.nic_model_type = configfile["libvirt_nic_model_type"] end end if configfile.has_key? "synced_folder_type" synced_folder_type = configfile["synced_folder_type"] else synced_folder_type = '9p' end config.vm.synced_folder './', '/vagrant', type: synced_folder_type else abort("No supported VM Provider found, set vm_provider in Vagrantfile.yaml!") end config.vm.boot_timeout = configfile['boot_timeout'] if configfile.has_key? "aptproxy" config.vm.provision :shell, path: "provision-apt-proxy", args: [configfile["aptproxy"]] end # buildserver/ is shared to the VM's /vagrant by default so the old # default does not need a custom mount if configfile["cachedir"] != "buildserver/cache" config.vm.synced_folder configfile["cachedir"], '/vagrant/cache', create: true, type: synced_folder_type end # Make sure dir exists to mount to, since buildserver/ is # automatically mounted as /vagrant in the guest VM. This is more # necessary with 9p synced folders Dir.mkdir('cache') unless File.exists?('cache') # cache .deb packages on the host via a mount trick if configfile.has_key? "aptcachedir" config.vm.synced_folder configfile["aptcachedir"], "/var/cache/apt/archives", owner: 'root', group: 'root', create: true end config.vm.provision "shell", path: "setup-env-vars", args: ["/home/vagrant/android-sdk"] config.vm.provision "shell", path: "provision-apt-get-install", args: [configfile['debian_mirror']] config.vm.provision "shell", path: "provision-android-sdk" config.vm.provision "shell", path: "provision-android-ndk", args: ["/home/vagrant/android-ndk"] config.vm.provision "shell", path: "provision-gradle" end fdroidserver-1.1.6/buildserver/config.buildserver.py0000644000175000017500000000115413576156546022636 0ustar hanshans00000000000000sdk_path = "/home/vagrant/android-sdk" ndk_paths = { 'r10e': "/home/vagrant/android-ndk/r10e", 'r11c': "/home/vagrant/android-ndk/r11c", 'r12b': "/home/vagrant/android-ndk/r12b", 'r13b': "/home/vagrant/android-ndk/r13b", 'r14b': "/home/vagrant/android-ndk/r14b", 'r15c': "/home/vagrant/android-ndk/r15c", 'r16b': "/home/vagrant/android-ndk/r16b", 'r17b': "/home/vagrant/android-ndk/r17b", 'r18b': "/home/vagrant/android-ndk/r18b", 'r19': "/home/vagrant/android-ndk/r19", } java_paths = { '8': "/usr/lib/jvm/java-8-openjdk-amd64", } gradle_version_dir = "/opt/gradle/versions" fdroidserver-1.1.6/buildserver/provision-android-ndk0000644000175000017500000000110313576156546022627 0ustar hanshans00000000000000#!/bin/bash # echo $0 set -e set -x NDK_BASE=$1 test -e $NDK_BASE || mkdir -p $NDK_BASE cd $NDK_BASE if [ ! -e $NDK_BASE/r10e ]; then 7zr x /vagrant/cache/android-ndk-r10e-linux-x86_64.bin > /dev/null mv android-ndk-r10e r10e fi for version in r11c r12b r13b r14b r15c r16b r17b r18b r19; do if [ ! -e ${NDK_BASE}/${version} ]; then unzip /vagrant/cache/android-ndk-${version}-linux-x86_64.zip > /dev/null mv android-ndk-${version} ${version} fi done chmod -R a+rX $NDK_BASE/ find $NDK_BASE/ -type f -executable -print0 | xargs -0 chmod a+x fdroidserver-1.1.6/buildserver/provision-android-sdk0000644000175000017500000001107613576156546022646 0ustar hanshans00000000000000#!/bin/bash # echo $0 set -e set -x if [ -z $ANDROID_HOME ]; then echo "ANDROID_HOME env var must be set!" exit 1 fi # TODO remove the rm, this should work with an existing ANDROID_HOME if [ ! -x $ANDROID_HOME/tools/android ]; then rm -rf $ANDROID_HOME mkdir ${ANDROID_HOME} mkdir ${ANDROID_HOME}/temp mkdir ${ANDROID_HOME}/platforms mkdir ${ANDROID_HOME}/build-tools cd $ANDROID_HOME tools=`ls -1 /vagrant/cache/tools_*.zip | sort -n | tail -1` unzip -qq $tools fi # disable the repositories of proprietary stuff disabled=" @version@=1 @disabled@https\://dl.google.com/android/repository/extras/intel/addon.xml=disabled @disabled@https\://dl.google.com/android/repository/glass/addon.xml=disabled @disabled@https\://dl.google.com/android/repository/sys-img/android/sys-img.xml=disabled @disabled@https\://dl.google.com/android/repository/sys-img/android-tv/sys-img.xml=disabled @disabled@https\://dl.google.com/android/repository/sys-img/android-wear/sys-img.xml=disabled @disabled@https\://dl.google.com/android/repository/sys-img/google_apis/sys-img.xml=disabled " test -d ${HOME}/.android || mkdir ${HOME}/.android # there are currently zero user repos echo 'count=0' > ${HOME}/.android/repositories.cfg for line in $disabled; do echo $line >> ${HOME}/.android/sites-settings.cfg done cd /vagrant/cache # make links for `android update sdk` to use and delete blacklist="build-tools_r17-linux.zip build-tools_r18.0.1-linux.zip build-tools_r18.1-linux.zip build-tools_r18.1.1-linux.zip build-tools_r19-linux.zip build-tools_r19.0.1-linux.zip build-tools_r19.0.2-linux.zip build-tools_r19.0.3-linux.zip build-tools_r21-linux.zip build-tools_r21.0.1-linux.zip build-tools_r21.0.2-linux.zip build-tools_r21.1-linux.zip build-tools_r21.1.1-linux.zip build-tools_r22-linux.zip build-tools_r23-linux.zip android-1.5_r04-linux.zip android-1.6_r03-linux.zip android-2.0_r01-linux.zip android-2.0.1_r01-linux.zip" latestm2=`ls -1 android_m2repository*.zip | sort -n | tail -1` for f in $latestm2 android-[0-9]*.zip platform-[0-9]*.zip build-tools_r*-linux.zip; do rm -f ${ANDROID_HOME}/temp/$f if [[ $blacklist != *$f* ]]; then ln -s /vagrant/cache/$f ${ANDROID_HOME}/temp/ fi done # install all cached platforms cached="" for f in `ls -1 android-[0-9]*.zip platform-[0-9]*.zip`; do sdk=`unzip -c $f "*/build.prop" | sed -n 's,^ro.build.version.sdk=,,p'` cached=,android-${sdk}${cached} done # install all cached build-tools for f in `ls -1 build-tools*.zip`; do ver=`unzip -c $f "*/source.properties" | sed -n 's,^Pkg.Revision=,,p'` if [[ $ver == 24.0.0 ]] && [[ $f =~ .*r24\.0\.1.* ]]; then # 24.0.1 has the wrong revision in the zip ver=24.0.1 fi cached=,build-tools-${ver}${cached} done ${ANDROID_HOME}/tools/android update sdk --no-ui --all \ --filter platform-tools,extra-android-m2repository${cached} < $ANDROID_HOME/licenses/android-sdk-license 8933bad161af4178b1185d1a37fbf41ea5269c55 d56f5187479451eabf01fb78af6dfcb131a6481e EOF cat < $ANDROID_HOME/licenses/android-sdk-preview-license 84831b9409646a918e30573bab4c9c91346d8abd EOF cat < $ANDROID_HOME/licenses/android-sdk-preview-license-old 79120722343a6f314e0719f863036c702b0e6b2a 84831b9409646a918e30573bab4c9c91346d8abd EOF echo y | $ANDROID_HOME/tools/bin/sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout;1.0.1" echo y | $ANDROID_HOME/tools/bin/sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.1" echo y | $ANDROID_HOME/tools/bin/sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2" echo y | $ANDROID_HOME/tools/bin/sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.2" chmod -R a+rX $ANDROID_HOME/ chgrp vagrant $ANDROID_HOME chmod g+w $ANDROID_HOME find $ANDROID_HOME/ -type f -executable -print0 | xargs -0 chmod a+x # allow gradle to install newer build-tools and platforms chgrp vagrant $ANDROID_HOME/{build-tools,platforms} chmod g+w $ANDROID_HOME/{build-tools,platforms} # allow gradle/sdkmanager to install into the new m2repository test -d $ANDROID_HOME/extras/m2repository || mkdir -p $ANDROID_HOME/extras/m2repository find $ANDROID_HOME/extras/m2repository -type d | xargs chgrp vagrant find $ANDROID_HOME/extras/m2repository -type d | xargs chmod g+w fdroidserver-1.1.6/buildserver/provision-apt-get-install0000644000175000017500000000504613576156531023446 0ustar hanshans00000000000000#!/bin/bash echo $0 set -e set -x debian_mirror=$1 export DEBIAN_FRONTEND=noninteractive printf 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";\n' \ > /etc/apt/apt.conf.d/99no-install-recommends printf 'APT::Acquire::Retries "20";\n' \ > /etc/apt/apt.conf.d/99acquire-retries cat < /etc/apt/apt.conf.d/99no-auto-updates APT::Periodic::Enable "0"; APT::Periodic::Update-Package-Lists "0"; APT::Periodic::Unattended-Upgrade "0"; EOF printf 'APT::Get::Assume-Yes "true";\n' \ > /etc/apt/apt.conf.d/99assumeyes if echo $debian_mirror | grep '^https' 2>&1 > /dev/null; then apt-get update || apt-get update apt-get install apt-transport-https ca-certificates fi cat << EOF > /etc/apt/sources.list deb ${debian_mirror} stretch main deb http://security.debian.org/debian-security stretch/updates main deb ${debian_mirror} stretch-updates main EOF echo "deb ${debian_mirror} stretch-backports main" > /etc/apt/sources.list.d/stretch-backports.list echo "deb ${debian_mirror} testing main" > /etc/apt/sources.list.d/testing.list printf "Package: *\nPin: release o=Debian,a=testing\nPin-Priority: -300\n" > /etc/apt/preferences.d/debian-testing dpkg --add-architecture i386 apt-get update || apt-get update apt-get upgrade --download-only apt-get upgrade packages=" ant asn1c ant-contrib autoconf autoconf2.13 automake automake1.11 autopoint bison bzr ca-certificates-java cmake curl disorderfs expect faketime flex gettext gettext-base git-core git-svn gperf javacc libarchive-zip-perl libexpat1-dev libgcc1:i386 libglib2.0-dev liblzma-dev libncurses5:i386 librsvg2-bin libsaxonb-java libssl-dev libstdc++6:i386 libtool libtool-bin make maven mercurial nasm openjdk-8-jre-headless openjdk-8-jdk-headless optipng p7zip pkg-config python-gnupg python-lxml python-magic python-pip python-setuptools python3-defusedxml python3-git python3-gitdb python3-gnupg python3-pip python3-pyasn1 python3-pyasn1-modules python3-requests python3-setuptools python3-smmap python3-yaml python3-ruamel.yaml quilt rsync scons sqlite3 subversion swig unzip xsltproc yasm zip zlib1g:i386 " apt-get install $packages --download-only apt-get install $packages highestjava=`update-java-alternatives --list | sort -n | tail -1 | cut -d ' ' -f 1` update-java-alternatives --set $highestjava # configure headless openjdk to work without gtk accessability dependencies sed -i -e 's@\(assistive_technologies=org.GNOME.Accessibility.AtkWrapper\)@#\1@' /etc/java-8-openjdk/accessibility.properties fdroidserver-1.1.6/buildserver/provision-apt-proxy0000644000175000017500000000045213576156531022400 0ustar hanshans00000000000000#!/bin/bash echo $0 set -e rm -f /etc/apt/apt.conf.d/02proxy echo "Acquire::ftp::Proxy \"$1\";" >> /etc/apt/apt.conf.d/02proxy echo "Acquire::http::Proxy \"$1\";" >> /etc/apt/apt.conf.d/02proxy echo "Acquire::https::Proxy \"$1\";" >> /etc/apt/apt.conf.d/02proxy apt-get update || apt-get update fdroidserver-1.1.6/buildserver/provision-gradle0000644000175000017500000000141213576156531021670 0ustar hanshans00000000000000#!/bin/bash set -ex # version compare magic vergte() { printf '%s\n%s' "$1" "$2" | sort -C -V -r } test -e /opt/gradle/versions || mkdir -p /opt/gradle/versions cd /opt/gradle/versions for f in /vagrant/cache/gradle-*.zip; do ver=`echo $f | sed 's,.*gradle-\([0-9][0-9.]*\).*\.zip,\1,'` # only use versions greater or equal 2.2.1 if vergte $ver 2.2.1 && [ ! -d /opt/gradle/versions/${ver} ]; then unzip -qq $f mv gradle-${ver} /opt/gradle/versions/${ver} fi done chmod -R a+rX /opt/gradle test -e /opt/gradle/bin || mkdir -p /opt/gradle/bin ln -fs /home/vagrant/fdroidserver/gradlew-fdroid /opt/gradle/bin/gradle chown -h vagrant.vagrant /opt/gradle/bin/gradle chown vagrant.vagrant /opt/gradle/versions chmod 0755 /opt/gradle/versions fdroidserver-1.1.6/buildserver/setup-env-vars0000644000175000017500000000103713576156531021306 0ustar hanshans00000000000000#!/bin/sh # # sets up the environment vars needed by the build process set -e set -x bsenv=/etc/profile.d/bsenv.sh echo "# generated on "`date` > $bsenv echo export ANDROID_HOME=$1 >> $bsenv echo 'export PATH=$PATH:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:/opt/gradle/bin' >> $bsenv echo "export DEBIAN_FRONTEND=noninteractive" >> $bsenv chmod 0644 $bsenv # make sure that SSH never hangs at a password or key prompt printf ' StrictHostKeyChecking yes' >> /etc/ssh/ssh_config printf ' BatchMode yes' >> /etc/ssh/config fdroidserver-1.1.6/completion/0000755000175000017500000000000013576156553016312 5ustar hanshans00000000000000fdroidserver-1.1.6/completion/bash-completion0000644000175000017500000001470313576156531021322 0ustar hanshans00000000000000# fdroid(1) completion -*- shell-script -*- # # bash-completion - part of the FDroid server tools # # Copyright (C) 2013-2017 Hans-Christoph Steiner # Copyright (C) 2013, 2014 Daniel Martí # # 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 . __fdroid_init() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" (( $# >= 1 )) && __complete_${1} } __by_ext() { local ext="$1" files=( metadata/*.$ext ) files=( ${files[@]#metadata/} ) files=${files[@]%.$ext} echo "$files" } __package() { files="$(__by_ext txt) $(__by_ext yml) $(__by_ext json)" COMPREPLY=( $( compgen -W "$files" -- $cur ) ) } __apk_package() { files=( ${1}/*.apk ) [ -f "${files[0]}" ] || return files=( ${files[@]#*/} ) files=${files[@]%_*} COMPREPLY=( $( compgen -W "$files" -- $cur ) ) } __apk_vercode() { local p=${cur:0:-1} files=( ${1}/${p}_*.apk ) [ -f "${files[0]}" ] || return files=( ${files[@]#*_} ) files=${files[@]%.apk} COMPREPLY=( $( compgen -P "${p}:" -W "$files" -- $cur ) ) } __vercode() { local p v echo $cur | IFS=':' read p v COMPREPLY=( $( compgen -P "${p}:" -W "$( while read line; do if [[ "$line" == "Build Version:"* ]] then line="${line#*,}" printf "${line%%,*} " elif [[ "$line" == "Build:"* ]] then line="${line#*,}" printf "${line%%,*} " fi done < "metadata/${p}.txt" )" -- $cur ) ) } __complete_options() { case "${cur}" in --*) COMPREPLY=( $( compgen -W "--help --version ${lopts}" -- $cur ) ) return 0;; *) COMPREPLY=( $( compgen -W "-h ${opts} --help --version ${lopts}" -- $cur ) ) return 0;; esac } __complete_build() { opts="-v -q -l -s -t -f -a -w" lopts="--verbose --quiet --latest --stop --test --server --reset-server --skip-scan --no-tarball --force --all --wiki --no-refresh" case "${cur}" in -*) __complete_options return 0;; *:*) __vercode return 0;; *) __package return 0;; esac } __complete_dscanner() { opts="-v -q -l" lopts="--verbose --quiet --clean-after --clean-before --clean-only --init-only --latest --repo-path" case "${cur}" in -*) __complete_options return 0;; *:) __vercode return 0;; *) __package return 0;; esac } __complete_gpgsign() { opts="-v -q" lopts="--verbose --quiet" __complete_options } __complete_install() { opts="-v -q" lopts="--verbose --quiet --all" case "${cur}" in -*) __complete_options return 0;; *:) __apk_vercode repo return 0;; *) __apk_package repo return 0;; esac } __complete_update() { opts="-c -v -q -b -i -I -e -w" lopts="--create-metadata --verbose --quiet --buildreport --icons --wiki --pretty --clean --delete-unknown --nosign --rename-apks --use-date-from-apk" case "${prev}" in -e|--editor) _filedir return 0;; esac __complete_options } __complete_publish() { opts="-v -q" lopts="--verbose --quiet" case "${cur}" in -*) __complete_options return 0;; *:) __apk_vercode unsigned return 0;; *) __apk_package unsigned return 0;; esac } __complete_checkupdates() { opts="-v -q" lopts="--verbose --quiet --auto --autoonly --commit --gplay --allow-dirty" case "${cur}" in -*) __complete_options return 0;; *) __package return 0;; esac } __complete_import() { opts="-c -h -l -q -s -u -v -W" lopts="--categories --license --quiet --rev --subdir --url" case "${prev}" in -c|-l|-s|-u|--categories|--license|--quiet|--rev|--subdir|--url) return 0;; -W) COMPREPLY=( $( compgen -W "error warn ignore" -- $cur ) ) return 0;; esac __complete_options } __complete_readmeta() { opts="-v -q" lopts="--verbose --quiet" __complete_options } __complete_rewritemeta() { opts="-v -q -l" lopts="--verbose --quiet --list" case "${cur}" in -*) __complete_options return 0;; *) __package return 0;; esac } __complete_lint() { opts="-v -q" lopts="--verbose --quiet" case "${cur}" in -*) __complete_options return 0;; *) __package return 0;; esac } __complete_scanner() { opts="-v -q" lopts="--verbose --quiet" case "${cur}" in -*) __complete_options return 0;; *:) __vercode return 0;; *) __package return 0;; esac } __complete_verify() { opts="-v -q -p" lopts="--verbose --quiet" case "${cur}" in -*) __complete_options return 0;; *:) __vercode return 0;; *) __package return 0;; esac } __complete_btlog() { opts="-u" lopts="--git-remote --git-repo --url" __complete_options } __complete_mirror() { opts="-v" lopts="--archive --output-dir" __complete_options } __complete_nightly() { opts="-v -q" lopts="--show-secret-var --archive-older" __complete_options } __complete_stats() { opts="-v -q -d" lopts="--verbose --quiet --download" __complete_options } __complete_deploy() { opts="-i -v -q" lopts="--identity-file --local-copy-dir --sync-from-local-copy-dir --verbose --quiet --no-checksum --no-keep-git-mirror-archive" __complete_options } __complete_signatures() { opts="-v -q" lopts="--verbose --no-check-https" case "${cur}" in -*) __complete_options return 0;; esac } __complete_signindex() { opts="-v -q" lopts="--verbose" __complete_options } __complete_init() { opts="-v -q -d" lopts="--verbose --quiet --distinguished-name --keystore --repo-keyalias --android-home --no-prompt" __complete_options } __cmds=" \ btlog \ build \ checkupdates \ deploy \ dscanner \ gpgsign \ import \ init \ install \ lint \ mirror \ nightly \ publish \ readmeta \ rewritemeta \ scanner \ signatures \ signindex \ stats \ update \ verify \ " for c in $__cmds; do eval "_fdroid_${c} () { local cur prev opts lopts __fdroid_init ${c} }" done _fdroid() { local cmd cmd=${COMP_WORDS[1]} [[ $__cmds == *\ $cmd\ * ]] && _fdroid_${cmd} || { (($COMP_CWORD == 1)) && COMPREPLY=( $( compgen -W "${__cmds}" -- $cmd ) ) } } complete -F _fdroid fdroid return 0 fdroidserver-1.1.6/docker/0000755000175000017500000000000013576156553015410 5ustar hanshans00000000000000fdroidserver-1.1.6/docker/Dockerfile0000644000175000017500000001461213576156531017402 0ustar hanshans00000000000000# This image is intended to be used with fdroidserver for the purpose # of dynamic scanning of pre-built APKs during the fdroid build process. # Start with ubuntu 12.04 (i386). FROM ubuntu:14.04 MAINTAINER fdroid.dscanner ENV DROZER_URL https://github.com/mwrlabs/drozer/releases/download/2.3.4/drozer_2.3.4.deb ENV DROZER_DEB drozer_2.3.4.deb ENV AGENT_URL https://github.com/mwrlabs/drozer/releases/download/2.3.4/drozer-agent-2.3.4.apk ENV AGENT_APK drozer-agent-2.3.4.apk # Specially for SSH access and port redirection ENV ROOTPASSWORD android # Expose ADB, ADB control and VNC ports EXPOSE 22 EXPOSE 5037 EXPOSE 5554 EXPOSE 5555 EXPOSE 5900 EXPOSE 5901 ENV DEBIAN_FRONTEND noninteractive RUN echo "debconf shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections RUN echo "debconf shared/accepted-oracle-license-v1-1 seen true" | debconf-set-selections # Update packages RUN apt-get -y update # Drozer packages RUN apt-get install wget python2.7 python-dev python2.7-dev python-openssl python-twisted python-protobuf bash-completion -y # First, install add-apt-repository, sshd and bzip2 RUN apt-get -y install python-software-properties bzip2 ssh net-tools # ubuntu 14.04 needs this too RUN apt-get -y install software-properties-common # Add oracle-jdk7 to repositories RUN add-apt-repository ppa:webupd8team/java # Make sure the package repository is up to date RUN echo "deb http://archive.ubuntu.com/ubuntu trusty main universe" > /etc/apt/sources.list # Update apt RUN apt-get update # Add drozer RUN useradd -ms /bin/bash drozer # Install oracle-jdk7 RUN apt-get -y install oracle-java7-installer # Install android sdk RUN wget http://dl.google.com/android/android-sdk_r23-linux.tgz RUN tar -xvzf android-sdk_r23-linux.tgz RUN mv -v android-sdk-linux /usr/local/android-sdk # Install apache ant RUN wget http://archive.apache.org/dist/ant/binaries/apache-ant-1.8.4-bin.tar.gz RUN tar -xvzf apache-ant-1.8.4-bin.tar.gz RUN mv -v apache-ant-1.8.4 /usr/local/apache-ant # Add android tools and platform tools to PATH ENV ANDROID_HOME /usr/local/android-sdk ENV PATH $PATH:$ANDROID_HOME/tools ENV PATH $PATH:$ANDROID_HOME/platform-tools # Add ant to PATH ENV ANT_HOME /usr/local/apache-ant ENV PATH $PATH:$ANT_HOME/bin # Export JAVA_HOME variable ENV JAVA_HOME /usr/lib/jvm/java-7-oracle # Remove compressed files. RUN cd /; rm android-sdk_r23-linux.tgz && rm apache-ant-1.8.4-bin.tar.gz # Some preparation before update RUN chown -R root:root /usr/local/android-sdk/ # Install latest android tools and system images RUN echo "y" | android update sdk --filter platform-tool --no-ui --force RUN echo "y" | android update sdk --filter platform --no-ui --force RUN echo "y" | android update sdk --filter build-tools-22.0.1 --no-ui -a RUN echo "y" | android update sdk --filter sys-img-x86-android-19 --no-ui -a #RUN echo "y" | android update sdk --filter sys-img-x86-android-21 --no-ui -a #RUN echo "y" | android update sdk --filter sys-img-x86-android-22 --no-ui -a RUN echo "y" | android update sdk --filter sys-img-armeabi-v7a-android-19 --no-ui -a #RUN echo "y" | android update sdk --filter sys-img-armeabi-v7a-android-21 --no-ui -a #RUN echo "y" | android update sdk --filter sys-img-armeabi-v7a-android-22 --no-ui -a # Update ADB RUN echo "y" | android update adb # Create fake keymap file RUN mkdir /usr/local/android-sdk/tools/keymaps RUN touch /usr/local/android-sdk/tools/keymaps/en-us # Run sshd RUN apt-get install -y openssh-server RUN mkdir /var/run/sshd RUN echo "root:$ROOTPASSWORD" | chpasswd RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config RUN sed -i 's/PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config # SSH login fix. Otherwise user is kicked off after login RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd ENV NOTVISIBLE "in users profile" RUN echo "export VISIBLE=now" >> /etc/profile # Install socat RUN apt-get install -y socat # symlink android bins RUN ln -sv /usr/local/android-sdk/tools/android /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/emulator /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/ddms /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/scheenshot2 /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/monkeyrunner /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/monitor /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/mksdcard /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/uiautomatorviewer /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/tools/traceview /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/platform-tools/adb /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/platform-tools/fastboot /usr/local/bin/ RUN ln -sv /usr/local/android-sdk/platform-tools/sqlite3 /usr/local/bin/ # Setup DROZER... # https://labs.mwrinfosecurity.com/tools/drozer/ # Run as drozer user WORKDIR /home/drozer # Site lists the shasums, however, I'm not sure the best way to integrate the # checks here. No real idiomatic way for Dockerfile to do that and most of # the examples online use chained commands but we want things to *BREAK* when # the sha doesn't match. So far, I can't seem to reliably make Docker not # finish the image build process. # Download the console RUN wget -c $DROZER_URL # Install the console RUN dpkg -i $DROZER_DEB # Download agent RUN wget -c $AGENT_URL # Keep it version agnostic for other scripts such as install_drozer.py RUN mv -v $AGENT_APK drozer-agent.apk # Port forwarding required by drozer RUN echo 'adb forward tcp:31415 tcp:31415' >> /home/drozer/.bashrc # Alias for Drozer RUN echo "alias drozer='drozer console connect'" >> /home/drozer/.bashrc # add extra scripting COPY install_agent.py /home/drozer/install_agent.py RUN chmod 755 /home/drozer/install_agent.py COPY enable_service.py /home/drozer/enable_service.py RUN chmod 755 /home/drozer/enable_service.py COPY drozer.py /home/drozer/drozer.py RUN chmod 755 /home/drozer/drozer.py # fix ownerships RUN chown -R drozer.drozer /home/drozer RUN apt-get -y --force-yes install python-pkg-resources=3.3-1ubuntu1 RUN apt-get -y install python-pip python-setuptools git RUN pip install "git+https://github.com/dtmilano/AndroidViewClient.git#egg=androidviewclient" RUN apt-get -y install python-pexpect # Add entrypoint COPY entrypoint.sh /home/drozer/entrypoint.sh RUN chmod +x /home/drozer/entrypoint.sh ENTRYPOINT ["/home/drozer/entrypoint.sh"] fdroidserver-1.1.6/docker/Makefile0000644000175000017500000000232213576156531017043 0ustar hanshans00000000000000SHELL := /bin/bash ALIAS = "dscanner" EXISTS := $(shell docker ps -a -q -f name=$(ALIAS)) RUNNED := $(shell docker ps -q -f name=$(ALIAS)) ifneq "$(RUNNED)" "" IP := $(shell docker inspect $(ALIAS) | grep "IPAddress\"" | head -n1 | cut -d '"' -f 4) endif STALE_IMAGES := $(shell docker images | grep "" | awk '{print($$3)}') EMULATOR ?= "android-19" ARCH ?= "armeabi-v7a" COLON := : .PHONY = build clean kill info all: help help: @echo "usage: make {help|build|clean|kill|info}" @echo "" @echo " help this help screen" @echo " build create docker image" @echo " clean remove images and containers" @echo " kill stop running containers" @echo " info details of running container" build: @docker build -t "dscanner/fdroidserver:latest" . clean: kill @docker ps -a -q | xargs -n 1 -I {} docker rm -f {} ifneq "$(STALE_IMAGES)" "" @docker rmi -f $(STALE_IMAGES) endif kill: ifneq "$(RUNNED)" "" @docker kill $(ALIAS) endif info: @docker ps -a -f name=$(ALIAS) ifneq "$(RUNNED)" "" $(eval ADBPORT := $(shell docker port $(ALIAS) | grep '5555/tcp' | awk '{split($$3,a,"$(COLON)");print a[2]}')) @echo -e "Use:\n adb kill-server\n adb connect $(IP):$(ADBPORT)" else @echo "Run container" endif fdroidserver-1.1.6/docker/README.md0000644000175000017500000000044413576156531016665 0ustar hanshans00000000000000# dscanner docker image # Use `make help` for up-to-date instructions. ``` usage: make {help|build|clean|kill|info} help this help screen build create docker image clean remove images and containers kill stop running containers info details of running container ``` fdroidserver-1.1.6/docker/drozer.py0000644000175000017500000000236113576156531017265 0ustar hanshans00000000000000#!/usr/bin/env python2 import pexpect import sys prompt = "dz>" target = sys.argv[1] drozer = pexpect.spawn("drozer console connect") drozer.logfile = open("/tmp/drozer_report.log", "w") # start drozer.expect(prompt) def send_command(command, target): cmd = "run {0} -a {1}".format(command, target) drozer.sendline(cmd) drozer.expect(prompt) scanners = [ "scanner.misc.native", # Find native components included in packages #"scanner.misc.readablefiles", # Find world-readable files in the given folder #"scanner.misc.secretcodes", # Search for secret codes that can be used from the dialer #"scanner.misc.sflagbinaries", # Find suid/sgid binaries in the given folder (default is /system). #"scanner.misc.writablefiles", # Find world-writable files in the given folder "scanner.provider.finduris", # Search for content providers that can be queried. "scanner.provider.injection", # Test content providers for SQL injection vulnerabilities. "scanner.provider.sqltables", # Find tables accessible through SQL injection vulnerabilities. "scanner.provider.traversal" # Test content providers for basic directory traversal ] for scanner in scanners: send_command(scanner, target) fdroidserver-1.1.6/docker/enable_service.py0000755000175000017500000000050313576156531020725 0ustar hanshans00000000000000#!/usr/bin/env python2 from com.dtmilano.android.viewclient import ViewClient vc = ViewClient(*ViewClient.connectToDeviceOrExit()) button = vc.findViewWithText("OFF") if button: (x, y) = button.getXY() button.touch() else: print("Button not found. Is the app currently running?") exit() print("Done!") fdroidserver-1.1.6/docker/entrypoint.sh0000755000175000017500000000237513576156531020165 0ustar hanshans00000000000000#!/bin/bash if [[ $EMULATOR == "" ]]; then EMULATOR="android-19" echo "Using default emulator $EMULATOR" fi if [[ $ARCH == "" ]]; then ARCH="x86" echo "Using default arch $ARCH" fi echo EMULATOR = "Requested API: ${EMULATOR} (${ARCH}) emulator." if [[ -n $1 ]]; then echo "Last line of file specified as non-opt/last argument:" tail -1 $1 fi # Run sshd /usr/sbin/sshd adb start-server # Detect ip and forward ADB ports outside to outside interface ip=$(ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{ print $1}') socat tcp-listen:5037,bind=$ip,fork tcp:127.0.0.1:5037 & socat tcp-listen:5554,bind=$ip,fork tcp:127.0.0.1:5554 & socat tcp-listen:5555,bind=$ip,fork tcp:127.0.0.1:5555 & # Set up and run emulator if [[ $ARCH == *"x86"* ]] then EMU="x86" else EMU="arm" fi #FASTDROID_VNC_URL="https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/fastdroid-vnc/fastdroid-vnc" #wget -c "${FASTDROID_VNC_URL}" export PATH="${PATH}:/usr/local/android-sdk/tools/:/usr/local/android-sdk/platform-tools/" echo "no" | android create avd -f -n test -t ${EMULATOR} --abi default/${ARCH} echo "no" | emulator64-${EMU} -avd test -noaudio -no-window -gpu off -verbose -qemu -usbdevice tablet -vnc :0 fdroidserver-1.1.6/docker/install_agent.py0000755000175000017500000000351113576156531020605 0ustar hanshans00000000000000#!/usr/bin/env python2 import os from subprocess import call, check_output from time import sleep FNULL = open(os.devnull, 'w') print("Ensuring device is online") call("adb wait-for-device", shell=True) print("Installing the drozer agent") print("If the device just came online it is likely the package manager hasn't booted.") print("Will try multiple attempts to install.") print("This may need tweaking depending on hardware.") attempts = 0 time_to_sleep = 30 while attempts < 8: output = check_output('adb shell "pm list packages"', shell=True) print("Checking whether the package manager is up...") if "Could not access the Package Manager" in output: print("Nope. Sleeping for 30 seconds and then trying again.") sleep(time_to_sleep) else: break time_to_sleep = 5 attempts = 0 while attempts < 5: sleep(time_to_sleep) try: install_output = check_output("adb install /home/drozer/drozer-agent.apk", shell=True) except Exception: print("Failed. Trying again.") attempts += 1 else: attempts += 1 if "Error: Could not access the Package Manager" not in install_output: break print("Install attempted. Checking everything worked") pm_list_output = check_output('adb shell "pm list packages"', shell=True) if "com.mwr.dz" not in pm_list_output: print(install_output) exit("APK didn't install properly. Exiting.") print("Installed ok.") print("Starting the drozer agent main activity: com.mwr.dz/.activities.MainActivity") call('adb shell "am start com.mwr.dz/.activities.MainActivity"', shell=True, stdout=FNULL) print("Starting the service") # start the service call("python /home/drozer/enable_service.py", shell=True, stdout=FNULL) print("Forward dem ports mon.") call("adb forward tcp:31415 tcp:31415", shell=True, stdout=FNULL) fdroidserver-1.1.6/examples/0000755000175000017500000000000013576156553015757 5ustar hanshans00000000000000fdroidserver-1.1.6/examples/config.py0000644000175000017500000003141713576156546017606 0ustar hanshans00000000000000#!/usr/bin/env python3 # Copy this file to config.py, then amend the settings below according to # your system configuration. # Custom path to the Android SDK, defaults to $ANDROID_HOME # sdk_path = "$ANDROID_HOME" # Custom paths to various versions of the Android NDK, defaults to 'r12b' set # to $ANDROID_NDK. Most users will have the latest at $ANDROID_NDK, which is # used by default. If a version is missing or assigned to None, it is assumed # not installed. # ndk_paths = { # 'r10e': None, # 'r11c': None, # 'r12b': "$ANDROID_NDK", # 'r13b': None, # 'r14b': None, # 'r15c': None, # 'r16b': None, # 'r17b': None, # 'r18b': None, # 'r19': None, # } # Directory to store downloaded tools in (i.e. gradle versions) # By default, these are stored in ~/.cache/fdroidserver # cachedir = cache # java_paths = { # '8': "/usr/lib/jvm/java-8-openjdk", # } # Build tools version to be used # build_tools = "25.0.2" # Force all build to use the above version of build -tools, good for testing # builds without having all of the possible build-tools installed. # force_build_tools = True # Command or path to binary for running Ant # ant = "ant" # Command or path to binary for running maven 3 # mvn3 = "mvn" # Command or path to binary for running Gradle # Defaults to using an internal gradle wrapper (gradlew-fdroid). # gradle = "gradle" # Set the maximum age (in days) of an index that a client should accept from # this repo. Setting it to 0 or not setting it at all disables this # functionality. If you do set this to a non-zero value, you need to ensure # that your index is updated much more frequently than the specified interval. # The same policy is applied to the archive repo, if there is one. # repo_maxage = 0 repo_url = "https://MyFirstFDroidRepo.org/fdroid/repo" repo_name = "My First F-Droid Repo Demo" repo_icon = "fdroid-icon.png" repo_description = """ This is a repository of apps to be used with F-Droid. Applications in this repository are either official binaries built by the original application developers, or are binaries built from source by the admin of f-droid.org using the tools on https://gitlab.com/u/fdroid. """ # As above, but for the archive repo. # archive_older sets the number of versions kept in the main repo, with all # older ones going to the archive. Set it to 0, and there will be no archive # repository, and no need to define the other archive_ values. archive_older = 3 archive_url = "https://f-droid.org/archive" archive_name = "My First F-Droid Archive Demo" archive_icon = "fdroid-icon.png" archive_description = """ The repository of older versions of applications from the main demo repository. """ # This allows a specific kind of insecure APK to be included in the # 'repo' section. Since April 2017, APK signatures that use MD5 are # no longer considered valid, jarsigner and apksigner will return an # error when verifying. `fdroid update` will move APKs with these # disabled signatures to the archive. This option stops that # behavior, and lets those APKs stay part of 'repo'. # # allow_disabled_algorithms = True # Normally, all apps are collected into a single app repository, like on # https://f-droid.org. For certain situations, it is better to make a repo # that is made up of APKs only from a single app. For example, an automated # build server that publishes nightly builds. # per_app_repos = True # `fdroid update` will create a link to the current version of a given app. # This provides a static path to the current APK. To disable the creation of # this link, uncomment this: # make_current_version_link = False # By default, the "current version" link will be based on the "Name" of the # app from the metadata. You can change it to use a different field from the # metadata here: # current_version_name_source = 'packageName' # Optionally, override home directory for gpg # gpghome = '/home/fdroid/somewhere/else/.gnupg' # The ID of a GPG key for making detached signatures for apks. Optional. # gpgkey = '1DBA2E89' # The key (from the keystore defined below) to be used for signing the # repository itself. This is the same name you would give to keytool or # jarsigner using -alias. (Not needed in an unsigned repository). # repo_keyalias = "fdroidrepo" # Optionally, the public key for the key defined by repo_keyalias above can # be specified here. There is no need to do this, as the public key can and # will be retrieved from the keystore when needed. However, specifying it # manually can allow some processing to take place without access to the # keystore. # repo_pubkey = "..." # The keystore to use for release keys when building. This needs to be # somewhere safe and secure, and backed up! The best way to manage these # sensitive keys is to use a "smartcard" (aka Hardware Security Module). To # configure F-Droid to use a smartcard, set the keystore file using the keyword # "NONE" (i.e. keystore = "NONE"). That makes Java find the keystore on the # smartcard based on 'smartcardoptions' below. # keystore = "~/.local/share/fdroidserver/keystore.jks" # You should not need to change these at all, unless you have a very # customized setup for using smartcards in Java with keytool/jarsigner # smartcardoptions = "-storetype PKCS11 -providerName SunPKCS11-OpenSC \ # -providerClass sun.security.pkcs11.SunPKCS11 \ # -providerArg opensc-fdroid.cfg" # The password for the keystore (at least 6 characters). If this password is # different than the keypass below, it can be OK to store the password in this # file for real use. But in general, sensitive passwords should not be stored # in text files! # keystorepass = "password1" # The password for keys - the same is used for each auto-generated key as well # as for the repository key. You should not normally store this password in a # file since it is a sensitive password. # keypass = "password2" # The distinguished name used for all keys. # keydname = "CN=Birdman, OU=Cell, O=Alcatraz, L=Alcatraz, S=California, C=US" # Use this to override the auto-generated key aliases with specific ones # for particular applications. Normally, just leave it empty. # keyaliases = {} # keyaliases['com.example.app'] = 'example' # You can also force an app to use the same key alias as another one, using # the @ prefix. # keyaliases['com.example.another.plugin'] = '@com.example.another' # The full path to the root of the repository. It must be specified in # rsync/ssh format for a remote host/path. This is used for syncing a locally # generated repo to the server that is it hosted on. It must end in the # standard public repo name of "/fdroid", but can be in up to three levels of # sub-directories (i.e. /var/www/packagerepos/fdroid). You can include # multiple servers to sync to by wrapping the whole thing in {} or [], and # including the serverwebroot strings in a comma-separated list. # # serverwebroot = 'user@example:/var/www/fdroid' # serverwebroot = { # 'foo.com:/usr/share/nginx/www/fdroid', # 'bar.info:/var/www/fdroid', # } # Uncomment this option if you want to logs of builds and other processes to # your repository server(s). Logs get published to all servers configured in # 'serverwebroot'. For builds, only logs from build-jobs running inside a # buildserver VM are supported. # # deploy_process_logs = True # The full URL to a git remote repository. You can include # multiple servers to mirror to by wrapping the whole thing in {} or [], and # including the servergitmirrors strings in a comma-separated list. # Servers listed here will also be automatically inserted in the mirrors list. # # servergitmirrors = 'https://github.com/user/repo' # servergitmirrors = { # 'https://github.com/user/repo', # 'https://gitlab.com/user/repo', # } # Any mirrors of this repo, for example all of the servers declared in # serverwebroot and all the servers declared in servergitmirrors, # will automatically be used by the client. If one # mirror is not working, then the client will try another. If the # client has Tor enabled, then the client will prefer mirrors with # .onion addresses. This base URL will be used for both the main repo # and the archive, if it is enabled. So these URLs should end in the # 'fdroid' base of the F-Droid part of the web server like serverwebroot. # # mirrors = ( # 'https://foo.bar/fdroid', # 'http://foobarfoobarfoobar.onion/fdroid', # ) # optionally specify which identity file to use when using rsync or git over SSH # # identity_file = '~/.ssh/fdroid_id_rsa' # If you are running the repo signing process on a completely offline machine, # which provides the best security, then you can specify a folder to sync the # repo to when running `fdroid server update`. This is most likely going to # be a USB thumb drive, SD Card, or some other kind of removable media. Make # sure it is mounted before running `fdroid server update`. Using the # standard folder called 'fdroid' as the specified folder is recommended, like # with serverwebroot. # # local_copy_dir = '/media/MyUSBThumbDrive/fdroid' # If you are using local_copy_dir on an offline build/signing server, once the # thumb drive has been plugged into the online machine, it will need to be # synced to the copy on the online machine. To make that happen # automatically, set sync_from_local_copy_dir to True: # # sync_from_local_copy_dir = True # To upload the repo to an Amazon S3 bucket using `fdroid server # update`. Warning, this deletes and recreates the whole fdroid/ # directory each time. This prefers s3cmd, but can also use # apache-libcloud. To customize how s3cmd interacts with the cloud # provider, create a 's3cfg' file next to this file (config.py), and # those settings will be used instead of any 'aws' variable below. # # awsbucket = 'myawsfdroid' # awsaccesskeyid = 'SEE0CHAITHEIMAUR2USA' # awssecretkey = 'yourverysecretkeywordpassphraserighthere' # If you want to force 'fdroid server' to use a non-standard serverwebroot. # This will allow you to have 'serverwebroot' entries which do not end in # '/fdroid'. (Please note that some client features expect repository URLs # to end in '/fdroid/repo'.) # # nonstandardwebroot = False # If you want to upload the release apk file to androidobservatory.org # # androidobservatory = False # If you want to upload the release apk file to virustotal.com # You have to enter your profile apikey to enable the upload. # # virustotal_apikey = "virustotal_apikey" # The build logs can be posted to a mediawiki instance, like on f-droid.org. # wiki_protocol = "http" # wiki_server = "server" # wiki_path = "/wiki/" # wiki_user = "login" # wiki_password = "1234" # Keep a log of all generated index files in a git repo to provide a # "binary transparency" log for anyone to check the history of the # binaries that are published. This is in the form of a "git remote", # which this machine where `fdroid update` is run has already been # configured to allow push access (e.g. ssh key, username/password, etc) # binary_transparency_remote = "git@gitlab.com:fdroid/binary-transparency-log.git" # Only set this to true when running a repository where you want to generate # stats, and only then on the master build servers, not a development # machine. If you want to keep the "added" and "last updated" dates for each # app and APK in your repo, then you should enable this. # update_stats = True # When used with stats, this is a list of IP addresses that are ignored for # calculation purposes. # stats_ignore = [] # Server stats logs are retrieved from. Required when update_stats is True. # stats_server = "example.com" # User stats logs are retrieved from. Required when update_stats is True. # stats_user = "bob" # Use the following to push stats to a Carbon instance: # stats_to_carbon = False # carbon_host = '0.0.0.0' # carbon_port = 2003 # Set this to true to always use a build server. This saves specifying the # --server option on dedicated secure build server hosts. # build_server_always = True # By default, fdroid will use YAML .yml and the custom .txt metadata formats. It # is also possible to have metadata in JSON by adding 'json'. # accepted_formats = ('txt', 'yml') # Limit in number of characters that fields can take up # Only the fields listed here are supported, defaults shown # char_limits = { # 'author': 256, # 'name': 30, # 'summary': 80, # 'description': 4000, # 'video': 256, # 'whatsNew': 500, # } # It is possible for the server operator to specify lists of apps that # must be installed or uninstalled on the client (aka "push installs). # If the user has opted in, or the device is already setup to respond # to these requests, then F-Droid will automatically install/uninstall # the packageNames listed. This is protected by the same signing key # as the app index metadata. # # install_list = ( # 'at.bitfire.davdroid', # 'com.fsck.k9', # 'us.replicant', # ) # # uninstall_list = ( # 'com.facebook.orca', # 'com.android.vending', # ) fdroidserver-1.1.6/examples/fdroid-icon.png0000644000175000017500000000644713576156531020671 0ustar hanshans00000000000000PNG  IHDR00WsBIT|d pHYs a aJ%tEXtSoftwarewww.inkscape.org<tEXtTitleF-Droid logom{tEXtAuthorRobert Martinez1RtEXtCopyrightCC Attribution-ShareAlike http://creativecommons.org/licenses/by-sa/3.0/^Z IDAThՙ{mW]?ksqvmi) DhB4% D45&( 1)*?nc1TZN̝9Z}^wf:SkB^{{QU~c_x?yt3mOjWz~OA/|k3w]pPb`TЧjvS5AFxѵQ"g|tE_o _~K Wڑ$pUڪ, ` O}7>9ĝ+*)҇*]sӋ3~5Ɣ0X `LlLXlq7R}L8i$$!·8i·d<(je_,Ө~0{={/#yĄA J1leސ[?**QeWcoUY}a.8I$M>ëC!F,* C>31 xl*5>rhxT6pc0 ŕ$oÌ4uNr3_y'drAK*( cTId BK35vO/^o472ՂD=wK?tvm/}_na>ECDԭL'80;? ?a/)N2|gh hLa~ixt,z"b1h1D8U_8Is|.u]gg xgN dNȜys9K_?R4kåoGm#S\.3 CVCy.xwYwOw aDTN`Bqd@u1HRy5` 1jvw>k*Ita댍2>?&s~+4kOPNC^8-H7fwogzj -0bJ^3bՋ4$QbDgלWIEzkܸ˲nBn80˙3T.vnaљF .E"Ѿc FFxuݐvu=7;3ę tݝ@Y[^IZZKsz2q:zs5y̷ۨ CF:T󾜅P)Bb0&`~ql*")h@^uqED{/] ;r?-YZTJ%rk crjRKY(j6;hR?QzhO7P*@$R#,Ol6Ѩb Ρ(qcR{pi\g>9-jyj1&B\x[};8tWT- 8$DILms\}r3PVWVY]^2SgR!ZU$-,{XOb@ގNM",_[BzSrH^k*wsSҨ26#S8/'8`5@db> 1ze\t,s4FOq;`b4R -}hwe,cm"a?<:h0^P+&(^fav*: .,"Ɛipw+,axmo&i!@EZ<54g[\~%NONhPR[ cS #ocYDŽީ̫Q 7w^[asuMn*JK rEZZ#M-?(^吒zP#,6]$"MnF@^ZoPav84X#OB_JM֒KQ%SH1X 7)קbp *(> KV-qſeV[*[ڋW HkdO}HR}eBJ_۸!(A.gәg]};'n-\ y@nWIENDB`fdroidserver-1.1.6/examples/makebuildserver.config.py0000644000175000017500000000711313576156531022757 0ustar hanshans00000000000000#!/usr/bin/env python3 # # You may want to alter these before running ./makebuildserver # Name of the Vagrant basebox to use, by default it will be downloaded # from Vagrant Cloud. For release builds setup, generate the basebox # locally using https://gitlab.com/fdroid/basebox, add it to Vagrant, # then set this to the local basebox name. # This defaults to "fdroid/basebox-stretch64" which will download a # prebuilt basebox from https://app.vagrantup.com/fdroid. # # (If you change this value you have to supply the `--clean` option on # your next `makebuildserver` run.) # # basebox = "basebox-stretch64" # This allows you to pin your basebox to a specific versions. It defaults # the most recent basebox version which can be aumotaically verifyed by # `makebuildserver`. # Please note that vagrant does not support versioning of locally added # boxes, so we can't support that either. # # (If you change this value you have to supply the `--clean` option on # your next `makebuildserver` run.) # # basebox_version = "0.1" # In the process of setting up the build server, many gigs of files # are downloaded (Android SDK components, gradle, etc). These are # cached so that they are not redownloaded each time. By default, # these are stored in ~/.cache/fdroidserver # # cachedir = 'buildserver/cache' # A big part of creating a new instance is downloading packages from Debian. # This setups up a folder in ~/.cache/fdroidserver to cache the downloaded # packages when rebuilding the build server from scratch. This requires # that virtualbox-guest-utils is installed. # # apt_package_cache = True # The buildserver can use some local caches to speed up builds, # especially when the internet connection is slow and/or expensive. # If enabled, the buildserver setup will look for standard caches in # your HOME dir and copy them to the buildserver VM. Be aware: this # will reduce the isolation of the buildserver from your host machine, # so the buildserver will provide an environment only as trustworthy # as the host machine's environment. # # copy_caches_from_host = True # To specify which Debian mirror the build server VM should use, by # default it uses http.debian.net, which auto-detects which is the # best mirror to use. # # debian_mirror = 'http://ftp.uk.debian.org/debian/' # The amount of RAM the build server will have (default: 2048) # memory = 3584 # The number of CPUs the build server will have # cpus = 1 # Debian package proxy server - if you have one # aptproxy = "http://192.168.0.19:8000" # If this is running on an older machine or on a virtualized system, # it can run a lot slower. If the provisioning fails with a warning # about the timeout, extend the timeout here. (default: 600 seconds) # # boot_timeout = 1200 # By default, this whole process uses VirtualBox as the provider, but # QEMU+KVM is also supported via the libvirt plugin to vagrant. If # this is run within a KVM guest, then libvirt's QEMU+KVM will be used # automatically. It can also be manually enabled by uncommenting # below: # # vm_provider = 'libvirt' # By default libvirt uses 'virtio' for both network and disk drivers. # Some systems (eg. nesting VMware ESXi) do not support virtio. As a # workaround for such rare cases, this setting allows to configure # KVM/libvirt to emulate hardware rather than using virtio. # # libvirt_disk_bus = 'sata' # libvirt_nic_model_type = 'rtl8139' # Sometimes, it is not possible to use the 9p synced folder type with # libvirt, like if running a KVM buildserver instance inside of a # VMware ESXi guest. In that case, using NFS or another method is # required. # # synced_folder_type = 'nfs' fdroidserver-1.1.6/examples/opensc-fdroid.cfg0000644000175000017500000000017313576156531021171 0ustar hanshans00000000000000name = OpenSC description = SunPKCS11 w/ OpenSC Smart card Framework library = /usr/lib/opensc-pkcs11.so slotListIndex = 1 fdroidserver-1.1.6/examples/public-read-only-s3-bucket-policy.json0000644000175000017500000000035513576156531025112 0ustar hanshans00000000000000{ "Version":"2012-10-17", "Statement":[ {"Sid":"AddPerm", "Effect":"Allow", "Principal":"*", "Action":"s3:GetObject", "Resource":"arn:aws:s3:::examplebucket/fdroid/*" } ] } fdroidserver-1.1.6/examples/template.yml0000644000175000017500000000036513576156531020315 0ustar hanshans00000000000000AuthorName: . WebSite: '' Bitcoin: null Litecoin: null Donation: null License: Unknown Categories: - Internet IssueTracker: '' SourceCode: '' Changelog: '' Name: . Summary: . Description: | . Archive Policy: 2 versions Requires Root: No fdroidserver-1.1.6/fdroid0000755000175000017500000001472213576156546015346 0ustar hanshans00000000000000#!/usr/bin/env python3 # # fdroid.py - part of the FDroid server tools # Copyright (C) 2010-2015, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Marti # # 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 . import sys import os import locale import logging import fdroidserver.common import fdroidserver.metadata from fdroidserver import _ from argparse import ArgumentError from collections import OrderedDict commands = OrderedDict([ ("build", _("Build a package from source")), ("init", _("Quickly start a new repository")), ("publish", _("Sign and place packages in the repo")), ("gpgsign", _("Add PGP signatures using GnuPG for packages in repo")), ("update", _("Update repo information for new packages")), ("deploy", _("Interact with the repo HTTP server")), ("verify", _("Verify the integrity of downloaded packages")), ("checkupdates", _("Check for updates to applications")), ("import", _("Add a new application from its source code")), ("install", _("Install built packages on devices")), ("readmeta", _("Read all the metadata files and exit")), ("rewritemeta", _("Rewrite all the metadata files")), ("lint", _("Warn about possible metadata errors")), ("scanner", _("Scan the source code of a package")), ("dscanner", _("Dynamically scan APKs post build")), ("stats", _("Update the stats of the repo")), ("server", _("Old, deprecated name for fdroid deploy")), ("signindex", _("Sign indexes created using update --nosign")), ("btlog", _("Update the binary transparency log for a URL")), ("signatures", _("Extract signatures from APKs")), ("nightly", _("Set up an app build for a nightly build repo")), ("mirror", _("Download complete mirrors of small repos")), ]) def print_help(): print(_("usage: ") + _("fdroid [] [-h|--help|--version|]")) print("") print(_("Valid commands are:")) for cmd, summary in commands.items(): print(" " + cmd + ' ' * (15 - len(cmd)) + summary) print("") def main(): if len(sys.argv) <= 1: print_help() sys.exit(0) command = sys.argv[1] if command not in commands: if command in ('-h', '--help'): print_help() sys.exit(0) elif command == '--version': output = _('no version info found!') cmddir = os.path.realpath(os.path.dirname(__file__)) moduledir = os.path.realpath(os.path.dirname(fdroidserver.common.__file__) + '/..') if cmddir == moduledir: # running from git os.chdir(cmddir) if os.path.isdir('.git'): import subprocess try: output = subprocess.check_output(['git', 'describe'], stderr=subprocess.STDOUT, universal_newlines=True) except subprocess.CalledProcessError: output = 'git commit ' + subprocess.check_output(['git', 'rev-parse', 'HEAD'], universal_newlines=True) elif os.path.exists('setup.py'): import re m = re.search(r'''.*[\s,\(]+version\s*=\s*["']([0-9a-z.]+)["'].*''', open('setup.py').read(), flags=re.MULTILINE) if m: output = m.group(1) + '\n' else: from pkg_resources import get_distribution output = get_distribution('fdroidserver').version + '\n' print(output), sys.exit(0) else: print(_("Command '%s' not recognised.\n" % command)) print_help() sys.exit(1) verbose = any(s in sys.argv for s in ['-v', '--verbose']) quiet = any(s in sys.argv for s in ['-q', '--quiet']) # Helpful to differentiate warnings from errors even when on quiet logformat = '%(levelname)s: %(message)s' loglevel = logging.INFO if verbose: loglevel = logging.DEBUG elif quiet: loglevel = logging.WARN logging.basicConfig(format=logformat, level=loglevel) if verbose and quiet: logging.critical(_("Conflicting arguments: '--verbose' and '--quiet' " "can not be specified at the same time.")) sys.exit(1) # temporary workaround until server.py becomes deploy.py if command == 'deploy': command = 'server' sys.argv.insert(2, 'update') # Trick optparse into displaying the right usage when --help is used. sys.argv[0] += ' ' + command del sys.argv[1] mod = __import__('fdroidserver.' + command, None, None, [command]) system_langcode, system_encoding = locale.getdefaultlocale() if system_encoding.lower() not in ('utf-8', 'utf8'): logging.warn(_("Encoding is set to '{enc}' fdroid might run " "into encoding issues. Please set it to 'UTF-8' " "for best results.".format(enc=system_encoding))) try: mod.main() # These are ours, contain a proper message and are "expected" except (fdroidserver.common.FDroidException, fdroidserver.metadata.MetaDataException) as e: if verbose: raise else: logging.critical(str(e)) sys.exit(1) except ArgumentError as e: logging.critical(str(e)) sys.exit(1) except KeyboardInterrupt: print('') fdroidserver.common.force_exit(1) # These should only be unexpected crashes due to bugs in the code # str(e) often doesn't contain a reason, so just show the backtrace except Exception as e: logging.critical(_("Unknown exception found!")) raise e sys.exit(0) if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/0000755000175000017500000000000013576156553016637 5ustar hanshans00000000000000fdroidserver-1.1.6/fdroidserver/__init__.py0000644000175000017500000000120713576156531020744 0ustar hanshans00000000000000 import gettext import glob import os import sys # support running straight from git and standard installs rootpaths = [ os.path.realpath(os.path.join(os.path.dirname(__file__), '..')), os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'share')), os.path.join(sys.prefix, 'share'), ] localedir = None for rootpath in rootpaths: if len(glob.glob(os.path.join(rootpath, 'locale', '*', 'LC_MESSAGES', 'fdroidserver.mo'))) > 0: localedir = os.path.join(rootpath, 'locale') break gettext.bindtextdomain('fdroidserver', localedir) gettext.textdomain('fdroidserver') _ = gettext.gettext fdroidserver-1.1.6/fdroidserver/asynchronousfilereader/0000755000175000017500000000000013576156553023415 5ustar hanshans00000000000000fdroidserver-1.1.6/fdroidserver/asynchronousfilereader/__init__.py0000644000175000017500000000260413576156531025524 0ustar hanshans00000000000000""" AsynchronousFileReader ====================== Simple thread based asynchronous file reader for Python. see https://github.com/soxofaan/asynchronousfilereader MIT License Copyright (c) 2014 Stefaan Lippens """ __version__ = '0.2.1' import threading try: # Python 2 from Queue import Queue except ImportError: # Python 3 from queue import Queue class AsynchronousFileReader(threading.Thread): """ Helper class to implement asynchronous reading of a file in a separate thread. Pushes read lines on a queue to be consumed in another thread. """ def __init__(self, fd, queue=None, autostart=True): self._fd = fd if queue is None: queue = Queue() self.queue = queue threading.Thread.__init__(self) if autostart: self.start() def run(self): """ The body of the tread: read lines and put them on the queue. """ while True: line = self._fd.readline() if not line: break self.queue.put(line) def eof(self): """ Check whether there is no more content to expect. """ return not self.is_alive() and self.queue.empty() def readlines(self): """ Get currently available lines. """ while not self.queue.empty(): yield self.queue.get() fdroidserver-1.1.6/fdroidserver/btlog.py0000755000175000017500000002077513576156531020332 0ustar hanshans00000000000000#!/usr/bin/env python3 # # btlog.py - part of the FDroid server tools # Copyright (C) 2017, Hans-Christoph Steiner # # 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 . # This is for creating a binary transparency log in a git repo for any # F-Droid repo accessible via HTTP. It is meant to run very often, # even once a minute in a cronjob, so it uses HEAD requests and the # HTTP ETag to check if the file has changed. HEAD requests should # not count against the download counts. This pattern of a HEAD then # a GET is what fdroidclient uses to avoid ETags being abused as # cookies. This also uses the same HTTP User Agent as the F-Droid # client app so its not easy for the server to distinguish this from # the F-Droid client. import collections import defusedxml.minidom import git import glob import os import json import logging import requests import shutil import tempfile import zipfile from argparse import ArgumentParser from . import _ from . import common from . import server from .exception import FDroidException options = None def make_binary_transparency_log(repodirs, btrepo='binary_transparency', url=None, commit_title='fdroid update'): '''Log the indexes in a standalone git repo to serve as a "binary transparency" log. see: https://www.eff.org/deeplinks/2014/02/open-letter-to-tech-companies ''' logging.info('Committing indexes to ' + btrepo) if os.path.exists(os.path.join(btrepo, '.git')): gitrepo = git.Repo(btrepo) else: if not os.path.exists(btrepo): os.mkdir(btrepo) gitrepo = git.Repo.init(btrepo) if not url: url = common.config['repo_url'].rstrip('/') with open(os.path.join(btrepo, 'README.md'), 'w') as fp: fp.write(""" # Binary Transparency Log for %s This is a log of the signed app index metadata. This is stored in a git repo, which serves as an imperfect append-only storage mechanism. People can then check that any file that they received from that F-Droid repository was a publicly released file. For more info on this idea: * https://wiki.mozilla.org/Security/Binary_Transparency """ % url[:url.rindex('/')]) # strip '/repo' gitrepo.index.add(['README.md', ]) gitrepo.index.commit('add README') for repodir in repodirs: cpdir = os.path.join(btrepo, repodir) if not os.path.exists(cpdir): os.mkdir(cpdir) for f in ('index.xml', 'index-v1.json'): repof = os.path.join(repodir, f) if not os.path.exists(repof): continue dest = os.path.join(cpdir, f) if f.endswith('.xml'): doc = defusedxml.minidom.parse(repof) output = doc.toprettyxml(encoding='utf-8') with open(dest, 'wb') as f: f.write(output) elif f.endswith('.json'): with open(repof) as fp: output = json.load(fp, object_pairs_hook=collections.OrderedDict) with open(dest, 'w') as fp: json.dump(output, fp, indent=2) gitrepo.index.add([repof, ]) for f in ('index.jar', 'index-v1.jar'): repof = os.path.join(repodir, f) if not os.path.exists(repof): continue dest = os.path.join(cpdir, f) jarin = zipfile.ZipFile(repof, 'r') jarout = zipfile.ZipFile(dest, 'w') for info in jarin.infolist(): if info.filename.startswith('META-INF/'): jarout.writestr(info, jarin.read(info.filename)) jarout.close() jarin.close() gitrepo.index.add([repof, ]) output_files = [] for root, dirs, files in os.walk(repodir): for f in files: output_files.append(os.path.relpath(os.path.join(root, f), repodir)) output = collections.OrderedDict() for f in sorted(output_files): repofile = os.path.join(repodir, f) stat = os.stat(repofile) output[f] = ( stat.st_size, stat.st_ctime_ns, stat.st_mtime_ns, stat.st_mode, stat.st_uid, stat.st_gid, ) fslogfile = os.path.join(cpdir, 'filesystemlog.json') with open(fslogfile, 'w') as fp: json.dump(output, fp, indent=2) gitrepo.index.add([os.path.join(repodir, 'filesystemlog.json'), ]) for f in glob.glob(os.path.join(cpdir, '*.HTTP-headers.json')): gitrepo.index.add([os.path.join(repodir, os.path.basename(f)), ]) gitrepo.index.commit(commit_title) def main(): global options parser = ArgumentParser(usage="%(prog)s [options]") common.setup_global_opts(parser) parser.add_argument("--git-repo", default=os.path.join(os.getcwd(), 'binary_transparency'), help=_("Path to the git repo to use as the log")) parser.add_argument("-u", "--url", default='https://f-droid.org', help=_("The base URL for the repo to log (default: https://f-droid.org)")) parser.add_argument("--git-remote", default=None, help=_("Push the log to this git remote repository")) options = parser.parse_args() if options.verbose: logging.getLogger("requests").setLevel(logging.INFO) logging.getLogger("urllib3").setLevel(logging.INFO) else: logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) if not os.path.exists(options.git_repo): raise FDroidException( '"%s" does not exist! Create it, or use --git-repo' % options.git_repo) session = requests.Session() new_files = False repodirs = ('repo', 'archive') tempdirbase = tempfile.mkdtemp(prefix='.fdroid-btlog-') for repodir in repodirs: # TODO read HTTP headers for etag from git repo tempdir = os.path.join(tempdirbase, repodir) os.makedirs(tempdir, exist_ok=True) gitrepodir = os.path.join(options.git_repo, repodir) os.makedirs(gitrepodir, exist_ok=True) for f in ('index.jar', 'index.xml', 'index-v1.jar', 'index-v1.json'): dlfile = os.path.join(tempdir, f) dlurl = options.url + '/' + repodir + '/' + f http_headers_file = os.path.join(gitrepodir, f + '.HTTP-headers.json') headers = { 'User-Agent': 'F-Droid 0.102.3' } etag = None if os.path.exists(http_headers_file): with open(http_headers_file) as fp: etag = json.load(fp)['ETag'] r = session.head(dlurl, headers=headers, allow_redirects=False) if r.status_code != 200: logging.debug('HTTP Response (' + str(r.status_code) + '), did not download ' + dlurl) continue if etag and etag == r.headers.get('ETag'): logging.debug('ETag matches, did not download ' + dlurl) continue r = session.get(dlurl, headers=headers, allow_redirects=False) if r.status_code == 200: with open(dlfile, 'wb') as f: for chunk in r: f.write(chunk) dump = dict() for k, v in r.headers.items(): dump[k] = v with open(http_headers_file, 'w') as fp: json.dump(dump, fp, indent=2, sort_keys=True) new_files = True if new_files: os.chdir(tempdirbase) make_binary_transparency_log(repodirs, options.git_repo, options.url, 'fdroid btlog') if options.git_remote: server.push_binary_transparency(options.git_repo, options.git_remote) shutil.rmtree(tempdirbase, ignore_errors=True) if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/build.py0000644000175000017500000015525213576156546020324 0ustar hanshans00000000000000#!/usr/bin/env python3 # # build.py - part of the FDroid server tools # Copyright (C) 2010-2014, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # # 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 . import os import shutil import glob import subprocess import posixpath import re import resource import sys import tarfile import threading import traceback import time import requests import tempfile import argparse from configparser import ConfigParser import logging from gettext import ngettext from . import _ from . import common from . import net from . import metadata from . import scanner from . import vmtools from .common import FDroidPopen from .exception import FDroidException, BuildException, VCSException try: import paramiko except ImportError: pass # Note that 'force' here also implies test mode. def build_server(app, build, vcs, build_dir, output_dir, log_dir, force): """Do a build on the builder vm. :param app: app metadata dict :param build: :param vcs: version control system controller object :param build_dir: local source-code checkout of app :param output_dir: target folder for the build result :param force: """ global buildserverid try: paramiko except NameError as e: raise BuildException("Paramiko is required to use the buildserver") from e if options.verbose: logging.getLogger("paramiko").setLevel(logging.INFO) else: logging.getLogger("paramiko").setLevel(logging.WARN) sshinfo = vmtools.get_clean_builder('builder', options.reset_server) output = None try: if not buildserverid: try: buildserverid = subprocess.check_output(['vagrant', 'ssh', '-c', 'cat /home/vagrant/buildserverid'], cwd='builder').strip().decode() logging.debug(_('Fetched buildserverid from VM: {buildserverid}') .format(buildserverid=buildserverid)) except Exception as e: if type(buildserverid) is not str or not re.match('^[0-9a-f]{40}$', buildserverid): logging.info(subprocess.check_output(['vagrant', 'status'], cwd="builder")) raise FDroidException("Could not obtain buildserverid from buldserver VM. " "(stored inside the buildserver VM at '/home/vagrant/buildserverid') " "Please reset your buildserver, the setup VM is broken.") from e # Open SSH connection... logging.info("Connecting to virtual machine...") sshs = paramiko.SSHClient() sshs.set_missing_host_key_policy(paramiko.AutoAddPolicy()) sshs.connect(sshinfo['hostname'], username=sshinfo['user'], port=sshinfo['port'], timeout=300, look_for_keys=False, key_filename=sshinfo['idfile']) homedir = posixpath.join('/home', sshinfo['user']) # Get an SFTP connection... ftp = sshs.open_sftp() ftp.get_channel().settimeout(60) # Put all the necessary files in place... ftp.chdir(homedir) # Helper to copy the contents of a directory to the server... def send_dir(path): logging.debug("rsyncing " + path + " to " + ftp.getcwd()) # TODO this should move to `vagrant rsync` from >= v1.5 try: subprocess.check_output(['rsync', '--recursive', '--perms', '--links', '--quiet', '--rsh=' + 'ssh -o StrictHostKeyChecking=no' + ' -o UserKnownHostsFile=/dev/null' + ' -o LogLevel=FATAL' + ' -o IdentitiesOnly=yes' + ' -o PasswordAuthentication=no' + ' -p ' + str(sshinfo['port']) + ' -i ' + sshinfo['idfile'], path, sshinfo['user'] + "@" + sshinfo['hostname'] + ":" + ftp.getcwd()], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: raise FDroidException(str(e), e.output.decode()) logging.info("Preparing server for build...") serverpath = os.path.abspath(os.path.dirname(__file__)) ftp.mkdir('fdroidserver') ftp.chdir('fdroidserver') ftp.put(os.path.join(serverpath, '..', 'fdroid'), 'fdroid') ftp.put(os.path.join(serverpath, '..', 'gradlew-fdroid'), 'gradlew-fdroid') ftp.chmod('fdroid', 0o755) # nosec B103 permissions are appropriate ftp.chmod('gradlew-fdroid', 0o755) # nosec B103 permissions are appropriate send_dir(os.path.join(serverpath)) ftp.chdir(homedir) ftp.put(os.path.join(serverpath, '..', 'buildserver', 'config.buildserver.py'), 'config.py') ftp.chmod('config.py', 0o600) # Copy over the ID (head commit hash) of the fdroidserver in use... with open(os.path.join(os.getcwd(), 'tmp', 'fdroidserverid'), 'wb') as fp: fp.write(subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=serverpath)) ftp.put('tmp/fdroidserverid', 'fdroidserverid') # Copy the metadata - just the file for this app... ftp.mkdir('metadata') ftp.mkdir('srclibs') ftp.chdir('metadata') ftp.put(app.metadatapath, os.path.basename(app.metadatapath)) # And patches if there are any... if os.path.exists(os.path.join('metadata', app.id)): send_dir(os.path.join('metadata', app.id)) ftp.chdir(homedir) # Create the build directory... ftp.mkdir('build') ftp.chdir('build') ftp.mkdir('extlib') ftp.mkdir('srclib') # Copy any extlibs that are required... if build.extlibs: ftp.chdir(posixpath.join(homedir, 'build', 'extlib')) for lib in build.extlibs: lib = lib.strip() libsrc = os.path.join('build/extlib', lib) if not os.path.exists(libsrc): raise BuildException("Missing extlib {0}".format(libsrc)) lp = lib.split('/') for d in lp[:-1]: if d not in ftp.listdir(): ftp.mkdir(d) ftp.chdir(d) ftp.put(libsrc, lp[-1]) for _ignored in lp[:-1]: ftp.chdir('..') # Copy any srclibs that are required... srclibpaths = [] if build.srclibs: for lib in build.srclibs: srclibpaths.append( common.getsrclib(lib, 'build/srclib', basepath=True, prepare=False)) # If one was used for the main source, add that too. basesrclib = vcs.getsrclib() if basesrclib: srclibpaths.append(basesrclib) for name, number, lib in srclibpaths: logging.info("Sending srclib '%s'" % lib) ftp.chdir(posixpath.join(homedir, 'build', 'srclib')) if not os.path.exists(lib): raise BuildException("Missing srclib directory '" + lib + "'") fv = '.fdroidvcs-' + name ftp.put(os.path.join('build/srclib', fv), fv) send_dir(lib) # Copy the metadata file too... ftp.chdir(posixpath.join(homedir, 'srclibs')) ftp.put(os.path.join('srclibs', name + '.txt'), name + '.txt') # Copy the main app source code # (no need if it's a srclib) if (not basesrclib) and os.path.exists(build_dir): ftp.chdir(posixpath.join(homedir, 'build')) fv = '.fdroidvcs-' + app.id ftp.put(os.path.join('build', fv), fv) send_dir(build_dir) # Execute the build script... logging.info("Starting build...") chan = sshs.get_transport().open_session() chan.get_pty() cmdline = posixpath.join(homedir, 'fdroidserver', 'fdroid') cmdline += ' build --on-server' if force: cmdline += ' --force --test' if options.verbose: cmdline += ' --verbose' if options.skipscan: cmdline += ' --skip-scan' if options.notarball: cmdline += ' --no-tarball' cmdline += " %s:%s" % (app.id, build.versionCode) chan.exec_command('bash --login -c "' + cmdline + '"') # nosec B601 inputs are sanitized # Fetch build process output ... try: cmd_stdout = chan.makefile('rb', 1024) output = bytes() output += common.get_android_tools_version_log(build.ndk_path()).encode() while not chan.exit_status_ready(): line = cmd_stdout.readline() if line: if options.verbose: logging.debug("buildserver > " + str(line, 'utf-8').rstrip()) output += line else: time.sleep(0.05) for line in cmd_stdout.readlines(): if options.verbose: logging.debug("buildserver > " + str(line, 'utf-8').rstrip()) output += line finally: cmd_stdout.close() # Check build process exit status ... logging.info("...getting exit status") returncode = chan.recv_exit_status() if returncode != 0: if timeout_event.is_set(): message = "Timeout exceeded! Build VM force-stopped for {0}:{1}" else: message = "Build.py failed on server for {0}:{1}" raise BuildException(message.format(app.id, build.versionName), None if options.verbose else str(output, 'utf-8')) # Retreive logs... toolsversion_log = common.get_toolsversion_logname(app, build) try: ftp.chdir(posixpath.join(homedir, log_dir)) ftp.get(toolsversion_log, os.path.join(log_dir, toolsversion_log)) logging.debug('retrieved %s', toolsversion_log) except Exception as e: logging.warn('could not get %s from builder vm: %s' % (toolsversion_log, e)) # Retrieve the built files... logging.info("Retrieving build output...") if force: ftp.chdir(posixpath.join(homedir, 'tmp')) else: ftp.chdir(posixpath.join(homedir, 'unsigned')) apkfile = common.get_release_filename(app, build) tarball = common.getsrcname(app, build) try: ftp.get(apkfile, os.path.join(output_dir, apkfile)) if not options.notarball: ftp.get(tarball, os.path.join(output_dir, tarball)) except Exception: raise BuildException( "Build failed for {0}:{1} - missing output files".format( app.id, build.versionName), None if options.verbose else str(output, 'utf-8')) ftp.close() finally: # Suspend the build server. vm = vmtools.get_build_vm('builder') vm.suspend() # deploy logfile to repository web server if output: common.deploy_build_log_with_rsync(app.id, build.versionCode, output) else: logging.debug('skip publishing full build logs: ' 'no output present') def force_gradle_build_tools(build_dir, build_tools): for root, dirs, files in os.walk(build_dir): for filename in files: if not filename.endswith('.gradle'): continue path = os.path.join(root, filename) if not os.path.isfile(path): continue logging.debug("Forcing build-tools %s in %s" % (build_tools, path)) common.regsub_file(r"""(\s*)buildToolsVersion([\s=]+).*""", r"""\1buildToolsVersion\2'%s'""" % build_tools, path) def transform_first_char(string, method): """Uses method() on the first character of string.""" if len(string) == 0: return string if len(string) == 1: return method(string) return method(string[0]) + string[1:] def get_metadata_from_apk(app, build, apkfile): """get the required metadata from the built APK versionName is allowed to be a blank string, i.e. '' """ appid, versionCode, versionName = common.get_apk_id(apkfile) native_code = common.get_native_code(apkfile) if build.buildjni and build.buildjni != ['no'] and not native_code: raise BuildException("Native code should have been built but none was packaged") if build.novcheck: versionCode = build.versionCode versionName = build.versionName if not versionCode or versionName is None: raise BuildException("Could not find version information in build in output") if not appid: raise BuildException("Could not find package ID in output") if appid != app.id: raise BuildException("Wrong package ID - build " + appid + " but expected " + app.id) return versionCode, versionName def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh): """Do a build locally.""" ndk_path = build.ndk_path() if build.ndk or (build.buildjni and build.buildjni != ['no']): if not ndk_path: logging.critical("Android NDK version '%s' could not be found!" % build.ndk or 'r12b') logging.critical("Configured versions:") for k, v in config['ndk_paths'].items(): if k.endswith("_orig"): continue logging.critical(" %s: %s" % (k, v)) raise FDroidException() elif not os.path.isdir(ndk_path): logging.critical("Android NDK '%s' is not a directory!" % ndk_path) raise FDroidException() common.set_FDroidPopen_env(build) # create ..._toolsversion.log when running in builder vm if onserver: # before doing anything, run the sudo commands to setup the VM if build.sudo: logging.info("Running 'sudo' commands in %s" % os.getcwd()) p = FDroidPopen(['sudo', 'bash', '-x', '-c', build.sudo]) if p.returncode != 0: raise BuildException("Error running sudo command for %s:%s" % (app.id, build.versionName), p.output) p = FDroidPopen(['sudo', 'passwd', '--lock', 'root']) if p.returncode != 0: raise BuildException("Error locking root account for %s:%s" % (app.id, build.versionName), p.output) p = FDroidPopen(['sudo', 'SUDO_FORCE_REMOVE=yes', 'dpkg', '--purge', 'sudo']) if p.returncode != 0: raise BuildException("Error removing sudo for %s:%s" % (app.id, build.versionName), p.output) log_path = os.path.join(log_dir, common.get_toolsversion_logname(app, build)) with open(log_path, 'w') as f: f.write(common.get_android_tools_version_log(build.ndk_path())) else: if build.sudo: logging.warning('%s:%s runs this on the buildserver with sudo:\n\t%s' % (app.id, build.versionName, build.sudo)) # Prepare the source code... root_dir, srclibpaths = common.prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver, refresh) # We need to clean via the build tool in case the binary dirs are # different from the default ones p = None gradletasks = [] bmethod = build.build_method() if bmethod == 'maven': logging.info("Cleaning Maven project...") cmd = [config['mvn3'], 'clean', '-Dandroid.sdk.path=' + config['sdk_path']] if '@' in build.maven: maven_dir = os.path.join(root_dir, build.maven.split('@', 1)[1]) maven_dir = os.path.normpath(maven_dir) else: maven_dir = root_dir p = FDroidPopen(cmd, cwd=maven_dir) elif bmethod == 'gradle': logging.info("Cleaning Gradle project...") if build.preassemble: gradletasks += build.preassemble flavours = build.gradle if flavours == ['yes']: flavours = [] flavours_cmd = ''.join([transform_first_char(flav, str.upper) for flav in flavours]) gradletasks += ['assemble' + flavours_cmd + 'Release'] if config['force_build_tools']: force_gradle_build_tools(build_dir, config['build_tools']) for name, number, libpath in srclibpaths: force_gradle_build_tools(libpath, config['build_tools']) cmd = [config['gradle']] if build.gradleprops: cmd += ['-P' + kv for kv in build.gradleprops] cmd += ['clean'] p = FDroidPopen(cmd, cwd=root_dir, envs={"GRADLE_VERSION_DIR": config['gradle_version_dir'], "CACHEDIR": config['cachedir']}) elif bmethod == 'buildozer': pass elif bmethod == 'ant': logging.info("Cleaning Ant project...") p = FDroidPopen(['ant', 'clean'], cwd=root_dir) if p is not None and p.returncode != 0: raise BuildException("Error cleaning %s:%s" % (app.id, build.versionName), p.output) for root, dirs, files in os.walk(build_dir): def del_dirs(dl): for d in dl: if d in dirs: shutil.rmtree(os.path.join(root, d)) def del_files(fl): for f in fl: if f in files: os.remove(os.path.join(root, f)) if any(f in files for f in ['build.gradle', 'settings.gradle']): # Even when running clean, gradle stores task/artifact caches in # .gradle/ as binary files. To avoid overcomplicating the scanner, # manually delete them, just like `gradle clean` should have removed # the build/* dirs. del_dirs([os.path.join('build', 'android-profile'), os.path.join('build', 'generated'), os.path.join('build', 'intermediates'), os.path.join('build', 'outputs'), os.path.join('build', 'reports'), os.path.join('build', 'tmp'), '.gradle']) del_files(['gradlew', 'gradlew.bat']) if 'pom.xml' in files: del_dirs(['target']) if any(f in files for f in ['ant.properties', 'project.properties', 'build.xml']): del_dirs(['bin', 'gen']) if 'jni' in dirs: del_dirs(['obj']) if options.skipscan: if build.scandelete: raise BuildException("Refusing to skip source scan since scandelete is present") else: # Scan before building... logging.info("Scanning source for common problems...") count = scanner.scan_source(build_dir, build) if count > 0: if force: logging.warning(ngettext('Scanner found {} problem', 'Scanner found {} problems', count).format(count)) else: raise BuildException(ngettext( "Can't build due to {} error while scanning", "Can't build due to {} errors while scanning", count).format(count)) if not options.notarball: # Build the source tarball right before we build the release... logging.info("Creating source tarball...") tarname = common.getsrcname(app, build) tarball = tarfile.open(os.path.join(tmp_dir, tarname), "w:gz") def tarexc(t): return None if any(t.name.endswith(s) for s in ['.svn', '.git', '.hg', '.bzr']) else t tarball.add(build_dir, tarname, filter=tarexc) tarball.close() # Run a build command if one is required... if build.build: logging.info("Running 'build' commands in %s" % root_dir) cmd = common.replace_config_vars(build.build, build) # Substitute source library paths into commands... for name, number, libpath in srclibpaths: libpath = os.path.relpath(libpath, root_dir) cmd = cmd.replace('$$' + name + '$$', libpath) p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir) if p.returncode != 0: raise BuildException("Error running build command for %s:%s" % (app.id, build.versionName), p.output) # Build native stuff if required... if build.buildjni and build.buildjni != ['no']: logging.info("Building the native code") jni_components = build.buildjni if jni_components == ['yes']: jni_components = [''] cmd = [os.path.join(ndk_path, "ndk-build"), "-j1"] for d in jni_components: if d: logging.info("Building native code in '%s'" % d) else: logging.info("Building native code in the main project") manifest = os.path.join(root_dir, d, 'AndroidManifest.xml') if os.path.exists(manifest): # Read and write the whole AM.xml to fix newlines and avoid # the ndk r8c or later 'wordlist' errors. The outcome of this # under gnu/linux is the same as when using tools like # dos2unix, but the native python way is faster and will # work in non-unix systems. manifest_text = open(manifest, 'U').read() open(manifest, 'w').write(manifest_text) # In case the AM.xml read was big, free the memory del manifest_text p = FDroidPopen(cmd, cwd=os.path.join(root_dir, d)) if p.returncode != 0: raise BuildException("NDK build failed for %s:%s" % (app.id, build.versionName), p.output) p = None # Build the release... if bmethod == 'maven': logging.info("Building Maven project...") if '@' in build.maven: maven_dir = os.path.join(root_dir, build.maven.split('@', 1)[1]) else: maven_dir = root_dir mvncmd = [config['mvn3'], '-Dandroid.sdk.path=' + config['sdk_path'], '-Dmaven.jar.sign.skip=true', '-Dmaven.test.skip=true', '-Dandroid.sign.debug=false', '-Dandroid.release=true', 'package'] if build.target: target = build.target.split('-')[1] common.regsub_file(r'[0-9]*', r'%s' % target, os.path.join(root_dir, 'pom.xml')) if '@' in build.maven: common.regsub_file(r'[0-9]*', r'%s' % target, os.path.join(maven_dir, 'pom.xml')) p = FDroidPopen(mvncmd, cwd=maven_dir) bindir = os.path.join(root_dir, 'target') elif bmethod == 'buildozer': logging.info("Building Kivy project using buildozer...") # parse buildozer.spez spec = os.path.join(root_dir, 'buildozer.spec') if not os.path.exists(spec): raise BuildException("Expected to find buildozer-compatible spec at {0}" .format(spec)) defaults = {'orientation': 'landscape', 'icon': '', 'permissions': '', 'android.api': "19"} bconfig = ConfigParser(defaults, allow_no_value=True) bconfig.read(spec) # update spec with sdk and ndk locations to prevent buildozer from # downloading. loc_ndk = common.env['ANDROID_NDK'] loc_sdk = common.env['ANDROID_SDK'] if loc_ndk == '$ANDROID_NDK': loc_ndk = loc_sdk + '/ndk-bundle' bc_ndk = None bc_sdk = None try: bc_ndk = bconfig.get('app', 'android.sdk_path') except Exception: pass try: bc_sdk = bconfig.get('app', 'android.ndk_path') except Exception: pass if bc_sdk is None: bconfig.set('app', 'android.sdk_path', loc_sdk) if bc_ndk is None: bconfig.set('app', 'android.ndk_path', loc_ndk) fspec = open(spec, 'w') bconfig.write(fspec) fspec.close() logging.info("sdk_path = %s" % loc_sdk) logging.info("ndk_path = %s" % loc_ndk) p = None # execute buildozer cmd = ['buildozer', 'android', 'release'] try: p = FDroidPopen(cmd, cwd=root_dir) except Exception: pass # buidozer not installed ? clone repo and run if (p is None or p.returncode != 0): cmd = ['git', 'clone', 'https://github.com/kivy/buildozer.git'] p = subprocess.Popen(cmd, cwd=root_dir, shell=False) p.wait() if p.returncode != 0: raise BuildException("Distribute build failed") cmd = ['python', 'buildozer/buildozer/scripts/client.py', 'android', 'release'] p = FDroidPopen(cmd, cwd=root_dir) # expected to fail. # Signing will fail if not set by environnment vars (cf. p4a docs). # But the unsigned apk will be ok. p.returncode = 0 elif bmethod == 'gradle': logging.info("Building Gradle project...") cmd = [config['gradle']] if build.gradleprops: cmd += ['-P' + kv for kv in build.gradleprops] cmd += gradletasks p = FDroidPopen(cmd, cwd=root_dir, envs={"GRADLE_VERSION_DIR": config['gradle_version_dir'], "CACHEDIR": config['cachedir']}) elif bmethod == 'ant': logging.info("Building Ant project...") cmd = ['ant'] if build.antcommands: cmd += build.antcommands else: cmd += ['release'] p = FDroidPopen(cmd, cwd=root_dir) bindir = os.path.join(root_dir, 'bin') if p is not None and p.returncode != 0: raise BuildException("Build failed for %s:%s" % (app.id, build.versionName), p.output) logging.info("Successfully built version " + build.versionName + ' of ' + app.id) omethod = build.output_method() if omethod == 'maven': stdout_apk = '\n'.join([ line for line in p.output.splitlines() if any( a in line for a in ('.apk', '.ap_', '.jar'))]) m = re.match(r".*^\[INFO\] .*apkbuilder.*/([^/]*)\.apk", stdout_apk, re.S | re.M) if not m: m = re.match(r".*^\[INFO\] Creating additional unsigned apk file .*/([^/]+)\.apk[^l]", stdout_apk, re.S | re.M) if not m: m = re.match(r'.*^\[INFO\] [^$]*aapt \[package,[^$]*' + bindir + r'/([^/]+)\.ap[_k][,\]]', stdout_apk, re.S | re.M) if not m: m = re.match(r".*^\[INFO\] Building jar: .*/" + bindir + r"/(.+)\.jar", stdout_apk, re.S | re.M) if not m: raise BuildException('Failed to find output') src = m.group(1) src = os.path.join(bindir, src) + '.apk' elif omethod == 'buildozer': src = None for apks_dir in [ os.path.join(root_dir, '.buildozer', 'android', 'platform', 'build', 'dists', bconfig.get('app', 'title'), 'bin'), ]: for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']: apks = glob.glob(os.path.join(apks_dir, apkglob)) if len(apks) > 1: raise BuildException('More than one resulting apks found in %s' % apks_dir, '\n'.join(apks)) if len(apks) == 1: src = apks[0] break if src is not None: break if src is None: raise BuildException('Failed to find any output apks') elif omethod == 'gradle': src = None apk_dirs = [ # gradle plugin >= 3.0 os.path.join(root_dir, 'build', 'outputs', 'apk', 'release'), # gradle plugin < 3.0 and >= 0.11 os.path.join(root_dir, 'build', 'outputs', 'apk'), # really old path os.path.join(root_dir, 'build', 'apk'), ] # If we build with gradle flavours with gradle plugin >= 3.0 the apk will be in # a subdirectory corresponding to the flavour command used, but with different # capitalization. if flavours_cmd: apk_dirs.append(os.path.join(root_dir, 'build', 'outputs', 'apk', transform_first_char(flavours_cmd, str.lower), 'release')) for apks_dir in apk_dirs: for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']: apks = glob.glob(os.path.join(apks_dir, apkglob)) if len(apks) > 1: raise BuildException('More than one resulting apks found in %s' % apks_dir, '\n'.join(apks)) if len(apks) == 1: src = apks[0] break if src is not None: break if src is None: raise BuildException('Failed to find any output apks') elif omethod == 'ant': stdout_apk = '\n'.join([ line for line in p.output.splitlines() if '.apk' in line]) src = re.match(r".*^.*Creating (.+) for release.*$.*", stdout_apk, re.S | re.M).group(1) src = os.path.join(bindir, src) elif omethod == 'raw': output_path = common.replace_build_vars(build.output, build) globpath = os.path.join(root_dir, output_path) apks = glob.glob(globpath) if len(apks) > 1: raise BuildException('Multiple apks match %s' % globpath, '\n'.join(apks)) if len(apks) < 1: raise BuildException('No apks match %s' % globpath) src = os.path.normpath(apks[0]) # Make sure it's not debuggable... if common.is_apk_and_debuggable(src): raise BuildException("APK is debuggable") # By way of a sanity check, make sure the version and version # code in our new apk match what we expect... logging.debug("Checking " + src) if not os.path.exists(src): raise BuildException("Unsigned apk is not at expected location of " + src) if common.get_file_extension(src) == 'apk': vercode, version = get_metadata_from_apk(app, build, src) if version != build.versionName or vercode != build.versionCode: raise BuildException(("Unexpected version/version code in output;" " APK: '%s' / '%s', " " Expected: '%s' / '%s'") % (version, str(vercode), build.versionName, str(build.versionCode))) # Copy the unsigned apk to our destination directory for further # processing (by publish.py)... dest = os.path.join(output_dir, common.get_release_filename(app, build)) shutil.copyfile(src, dest) # Move the source tarball into the output directory... if output_dir != tmp_dir and not options.notarball: shutil.move(os.path.join(tmp_dir, tarname), os.path.join(output_dir, tarname)) def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir, srclib_dir, extlib_dir, tmp_dir, repo_dir, vcs, test, server, force, onserver, refresh): """ Build a particular version of an application, if it needs building. :param output_dir: The directory where the build output will go. Usually this is the 'unsigned' directory. :param repo_dir: The repo directory - used for checking if the build is necessary. :param also_check_dir: An additional location for checking if the build is necessary (usually the archive repo) :param test: True if building in test mode, in which case the build will always happen, even if the output already exists. In test mode, the output directory should be a temporary location, not any of the real ones. :returns: True if the build was done, False if it wasn't necessary. """ dest_file = common.get_release_filename(app, build) dest = os.path.join(output_dir, dest_file) dest_repo = os.path.join(repo_dir, dest_file) if not test: if os.path.exists(dest) or os.path.exists(dest_repo): return False if also_check_dir: dest_also = os.path.join(also_check_dir, dest_file) if os.path.exists(dest_also): return False if build.disable and not options.force: return False logging.info("Building version %s (%s) of %s" % ( build.versionName, build.versionCode, app.id)) if server: # When using server mode, still keep a local cache of the repo, by # grabbing the source now. vcs.gotorevision(build.commit, refresh) build_server(app, build, vcs, build_dir, output_dir, log_dir, force) else: build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh) return True def force_halt_build(timeout): """Halt the currently running Vagrant VM, to be called from a Timer""" logging.error(_('Force halting build after {0} sec timeout!').format(timeout)) timeout_event.set() vm = vmtools.get_build_vm('builder') vm.halt() def parse_commandline(): """Parse the command line. Returns options, parser.""" parser = argparse.ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]") common.setup_global_opts(parser) parser.add_argument("appid", nargs='*', help=_("applicationId with optional versionCode in the form APPID[:VERCODE]")) parser.add_argument("-l", "--latest", action="store_true", default=False, help=_("Build only the latest version of each package")) parser.add_argument("-s", "--stop", action="store_true", default=False, help=_("Make the build stop on exceptions")) parser.add_argument("-t", "--test", action="store_true", default=False, help=_("Test mode - put output in the tmp directory only, and always build, even if the output already exists.")) parser.add_argument("--server", action="store_true", default=False, help=_("Use build server")) parser.add_argument("--reset-server", action="store_true", default=False, help=_("Reset and create a brand new build server, even if the existing one appears to be ok.")) # this option is internal API for telling fdroid that # it's running inside a buildserver vm. parser.add_argument("--on-server", dest="onserver", action="store_true", default=False, help=argparse.SUPPRESS) parser.add_argument("--skip-scan", dest="skipscan", action="store_true", default=False, help=_("Skip scanning the source code for binaries and other problems")) parser.add_argument("--dscanner", action="store_true", default=False, help=_("Setup an emulator, install the APK on it and perform a Drozer scan")) parser.add_argument("--no-tarball", dest="notarball", action="store_true", default=False, help=_("Don't create a source tarball, useful when testing a build")) parser.add_argument("--no-refresh", dest="refresh", action="store_false", default=True, help=_("Don't refresh the repository, useful when testing a build with no internet connection")) parser.add_argument("-f", "--force", action="store_true", default=False, help=_("Force build of disabled apps, and carries on regardless of scan problems. Only allowed in test mode.")) parser.add_argument("-a", "--all", action="store_true", default=False, help=_("Build all applications available")) parser.add_argument("-w", "--wiki", default=False, action="store_true", help=_("Update the wiki")) metadata.add_metadata_arguments(parser) options = parser.parse_args() metadata.warnings_action = options.W # Force --stop with --on-server to get correct exit code if options.onserver: options.stop = True if options.force and not options.test: parser.error("option %s: Force is only allowed in test mode" % "force") return options, parser options = None config = None buildserverid = None fdroidserverid = None start_timestamp = time.gmtime() timeout_event = threading.Event() def main(): global options, config, buildserverid, fdroidserverid options, parser = parse_commandline() # The defaults for .fdroid.* metadata that is included in a git repo are # different than for the standard metadata/ layout because expectations # are different. In this case, the most common user will be the app # developer working on the latest update of the app on their own machine. local_metadata_files = common.get_local_metadata_files() if len(local_metadata_files) == 1: # there is local metadata in an app's source config = dict(common.default_config) # `fdroid build` should build only the latest version by default since # most of the time the user will be building the most recent update if not options.all: options.latest = True elif len(local_metadata_files) > 1: raise FDroidException("Only one local metadata file allowed! Found: " + " ".join(local_metadata_files)) else: if not os.path.isdir('metadata') and len(local_metadata_files) == 0: raise FDroidException("No app metadata found, nothing to process!") if not options.appid and not options.all: parser.error("option %s: If you really want to build all the apps, use --all" % "all") config = common.read_config(options) if config['build_server_always']: options.server = True if options.reset_server and not options.server: parser.error("option %s: Using --reset-server without --server makes no sense" % "reset-server") if options.onserver or not options.server: for d in ['build-tools', 'platform-tools', 'tools']: if not os.path.isdir(os.path.join(config['sdk_path'], d)): raise FDroidException(_("Android SDK '{path}' does not have '{dirname}' installed!") .format(path=config['sdk_path'], dirname=d)) log_dir = 'logs' if not os.path.isdir(log_dir): logging.info("Creating log directory") os.makedirs(log_dir) tmp_dir = 'tmp' if not os.path.isdir(tmp_dir): logging.info("Creating temporary directory") os.makedirs(tmp_dir) if options.test: output_dir = tmp_dir else: output_dir = 'unsigned' if not os.path.isdir(output_dir): logging.info("Creating output directory") os.makedirs(output_dir) binaries_dir = os.path.join(output_dir, 'binaries') if config['archive_older'] != 0: also_check_dir = 'archive' else: also_check_dir = None repo_dir = 'repo' build_dir = 'build' if not os.path.isdir(build_dir): logging.info("Creating build directory") os.makedirs(build_dir) srclib_dir = os.path.join(build_dir, 'srclib') extlib_dir = os.path.join(build_dir, 'extlib') # Read all app and srclib metadata pkgs = common.read_pkg_args(options.appid, True) allapps = metadata.read_metadata(not options.onserver, pkgs, options.refresh, sort_by_time=True) apps = common.read_app_args(options.appid, allapps, True) for appid, app in list(apps.items()): if (app.Disabled and not options.force) or not app.RepoType or not app.builds: del apps[appid] if not apps: raise FDroidException("No apps to process.") # make sure enough open files are allowed to process everything soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) if len(apps) > soft: try: soft = len(apps) * 2 if soft > hard: soft = hard resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard)) logging.debug(_('Set open file limit to {integer}') .format(integer=soft)) except (OSError, ValueError) as e: logging.warning(_('Setting open file limit failed: ') + str(e)) if options.latest: for app in apps.values(): for build in reversed(app.builds): if build.disable and not options.force: continue app.builds = [build] break if options.wiki: import mwclient site = mwclient.Site((config['wiki_protocol'], config['wiki_server']), path=config['wiki_path']) site.login(config['wiki_user'], config['wiki_password']) # Build applications... failed_apps = {} build_succeeded = [] # Only build for 36 hours, then stop gracefully. endtime = time.time() + 36 * 60 * 60 max_build_time_reached = False for appid, app in apps.items(): first = True for build in app.builds: if time.time() > endtime: max_build_time_reached = True break # Enable watchdog timer (2 hours by default). if build.timeout is None: timeout = 7200 else: timeout = int(build.timeout) if options.server and timeout > 0: logging.debug(_('Setting {0} sec timeout for this build').format(timeout)) timer = threading.Timer(timeout, force_halt_build, [timeout]) timeout_event.clear() timer.start() else: timer = None wikilog = None build_starttime = common.get_wiki_timestamp() tools_version_log = '' if not options.onserver: tools_version_log = common.get_android_tools_version_log(build.ndk_path()) try: # For the first build of a particular app, we need to set up # the source repo. We can reuse it on subsequent builds, if # there are any. if first: vcs, build_dir = common.setup_vcs(app) first = False logging.info("Using %s" % vcs.clientversion()) logging.debug("Checking " + build.versionName) if trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir, srclib_dir, extlib_dir, tmp_dir, repo_dir, vcs, options.test, options.server, options.force, options.onserver, options.refresh): toolslog = os.path.join(log_dir, common.get_toolsversion_logname(app, build)) if not options.onserver and os.path.exists(toolslog): with open(toolslog, 'r') as f: tools_version_log = ''.join(f.readlines()) os.remove(toolslog) if app.Binaries is not None: # This is an app where we build from source, and # verify the apk contents against a developer's # binary. We get that binary now, and save it # alongside our built one in the 'unsigend' # directory. if not os.path.isdir(binaries_dir): os.makedirs(binaries_dir) logging.info("Created directory for storing " "developer supplied reference " "binaries: '{path}'" .format(path=binaries_dir)) url = app.Binaries url = url.replace('%v', build.versionName) url = url.replace('%c', str(build.versionCode)) logging.info("...retrieving " + url) of = re.sub(r'.apk$', '.binary.apk', common.get_release_filename(app, build)) of = os.path.join(binaries_dir, of) try: net.download_file(url, local_filename=of) except requests.exceptions.HTTPError as e: raise FDroidException( 'Downloading Binaries from %s failed.' % url) from e # Now we check whether the build can be verified to # match the supplied binary or not. Should the # comparison fail, we mark this build as a failure # and remove everything from the unsigend folder. with tempfile.TemporaryDirectory() as tmpdir: unsigned_apk = \ common.get_release_filename(app, build) unsigned_apk = \ os.path.join(output_dir, unsigned_apk) compare_result = \ common.verify_apks(of, unsigned_apk, tmpdir) if compare_result: logging.debug('removing %s', unsigned_apk) os.remove(unsigned_apk) logging.debug('removing %s', of) os.remove(of) compare_result = compare_result.split('\n') line_count = len(compare_result) compare_result = compare_result[:299] if line_count > len(compare_result): line_difference = \ line_count - len(compare_result) compare_result.append('%d more lines ...' % line_difference) compare_result = '\n'.join(compare_result) raise FDroidException('compared built binary ' 'to supplied reference ' 'binary but failed', compare_result) else: logging.info('compared built binary to ' 'supplied reference binary ' 'successfully') build_succeeded.append(app) wikilog = "Build succeeded" except VCSException as vcse: reason = str(vcse).split('\n', 1)[0] if options.verbose else str(vcse) logging.error("VCS error while building app %s: %s" % ( appid, reason)) if options.stop: logging.debug("Error encoutered, stopping by user request.") common.force_exit(1) failed_apps[appid] = vcse wikilog = str(vcse) except FDroidException as e: with open(os.path.join(log_dir, appid + '.log'), 'a+') as f: f.write('\n\n============================================================\n') f.write('versionCode: %s\nversionName: %s\ncommit: %s\n' % (build.versionCode, build.versionName, build.commit)) f.write('Build completed at ' + common.get_wiki_timestamp() + '\n') f.write('\n' + tools_version_log + '\n') f.write(str(e)) logging.error("Could not build app %s: %s" % (appid, e)) if options.stop: logging.debug("Error encoutered, stopping by user request.") common.force_exit(1) failed_apps[appid] = e wikilog = e.get_wikitext() except Exception as e: logging.error("Could not build app %s due to unknown error: %s" % ( appid, traceback.format_exc())) if options.stop: logging.debug("Error encoutered, stopping by user request.") common.force_exit(1) failed_apps[appid] = e wikilog = str(e) if options.wiki and wikilog: try: # Write a page with the last build log for this version code lastbuildpage = appid + '/lastbuild_' + build.versionCode newpage = site.Pages[lastbuildpage] with open(os.path.join('tmp', 'fdroidserverid')) as fp: fdroidserverid = fp.read().rstrip() txt = "* build session started at " + common.get_wiki_timestamp(start_timestamp) + '\n' \ + "* this build started at " + build_starttime + '\n' \ + "* this build completed at " + common.get_wiki_timestamp() + '\n' \ + common.get_git_describe_link() \ + '* fdroidserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \ + fdroidserverid + ' ' + fdroidserverid + ']\n\n' if buildserverid: txt += '* buildserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \ + buildserverid + ' ' + buildserverid + ']\n\n' txt += tools_version_log + '\n\n' txt += '== Build Log ==\n\n' + wikilog newpage.save(txt, summary='Build log') # Redirect from /lastbuild to the most recent build log newpage = site.Pages[appid + '/lastbuild'] newpage.save('#REDIRECT [[' + lastbuildpage + ']]', summary='Update redirect') except Exception as e: logging.error("Error while attempting to publish build log: %s" % e) if timer: timer.cancel() # kill the watchdog timer if max_build_time_reached: logging.info("Stopping after global build timeout...") break for app in build_succeeded: logging.info("success: %s" % (app.id)) if not options.verbose: for fa in failed_apps: logging.info("Build for app %s failed:\n%s" % (fa, failed_apps[fa])) # perform a drozer scan of all successful builds if options.dscanner and build_succeeded: from .dscanner import DockerDriver docker = DockerDriver() try: for app in build_succeeded: logging.info("Need to sign the app before we can install it.") subprocess.call("fdroid publish {0}".format(app.id)) apk_path = None for f in os.listdir(repo_dir): if f.endswith('.apk') and f.startswith(app.id): apk_path = os.path.join(repo_dir, f) break if not apk_path: raise Exception("No signed APK found at path: {path}".format(path=apk_path)) if not os.path.isdir(repo_dir): logging.critical("directory does not exists '{path}'".format(path=repo_dir)) common.force_exit(1) logging.info("Performing Drozer scan on {0}.".format(app)) docker.perform_drozer_scan(apk_path, app.id, repo_dir) except Exception as e: logging.error(str(e)) logging.error("An exception happened. Making sure to clean up") else: logging.info("Scan succeeded.") logging.info("Cleaning up after ourselves.") docker.clean() logging.info(_("Finished")) if len(build_succeeded) > 0: logging.info(ngettext("{} build succeeded", "{} builds succeeded", len(build_succeeded)).format(len(build_succeeded))) if len(failed_apps) > 0: logging.info(ngettext("{} build failed", "{} builds failed", len(failed_apps)).format(len(failed_apps))) if options.wiki: wiki_page_path = 'build_' + time.strftime('%s', start_timestamp) newpage = site.Pages[wiki_page_path] txt = '' txt += "* command line: %s\n" % ' '.join(sys.argv) txt += "* started at %s\n" % common.get_wiki_timestamp(start_timestamp) txt += "* completed at %s\n" % common.get_wiki_timestamp() if buildserverid: txt += ('* buildserverid: [https://gitlab.com/fdroid/fdroidserver/commit/{id} {id}]\n' .format(id=buildserverid)) if fdroidserverid: txt += ('* fdroidserverid: [https://gitlab.com/fdroid/fdroidserver/commit/{id} {id}]\n' .format(id=fdroidserverid)) if os.cpu_count(): txt += "* host processors: %d\n" % os.cpu_count() if os.path.isfile('/proc/meminfo') and os.access('/proc/meminfo', os.R_OK): with open('/proc/meminfo') as fp: for line in fp: m = re.search(r'MemTotal:\s*([0-9].*)', line) if m: txt += "* host RAM: %s\n" % m.group(1) break txt += "* successful builds: %d\n" % len(build_succeeded) txt += "* failed builds: %d\n" % len(failed_apps) txt += "\n\n" newpage.save(txt, summary='Run log') newpage = site.Pages['build'] newpage.save('#REDIRECT [[' + wiki_page_path + ']]', summary='Update redirect') # hack to ensure this exits, even is some threads are still running common.force_exit() if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/checkupdates.py0000644000175000017500000005732113576156546021666 0ustar hanshans00000000000000#!/usr/bin/env python3 # # checkupdates.py - part of the FDroid server tools # Copyright (C) 2010-2015, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # # 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 . import os import re import urllib.request import urllib.error import time import subprocess import sys from argparse import ArgumentParser import traceback import html from distutils.version import LooseVersion import logging import copy import urllib.parse from . import _ from . import common from . import metadata from .exception import VCSException, NoSubmodulesException, FDroidException, MetaDataException # Check for a new version by looking at a document retrieved via HTTP. # The app's Update Check Data field is used to provide the information # required. def check_http(app): try: if not app.UpdateCheckData: raise FDroidException('Missing Update Check Data') urlcode, codeex, urlver, verex = app.UpdateCheckData.split('|') parsed = urllib.parse.urlparse(urlcode) if not parsed.netloc or not parsed.scheme or parsed.scheme != 'https': raise FDroidException(_('UpdateCheckData has invalid URL: {url}').format(url=urlcode)) if urlver != '.': parsed = urllib.parse.urlparse(urlver) if not parsed.netloc or not parsed.scheme or parsed.scheme != 'https': raise FDroidException(_('UpdateCheckData has invalid URL: {url}').format(url=urlcode)) vercode = None if len(urlcode) > 0: logging.debug("...requesting {0}".format(urlcode)) req = urllib.request.Request(urlcode, None) resp = urllib.request.urlopen(req, None, 20) page = resp.read().decode('utf-8') m = re.search(codeex, page) if not m: raise FDroidException("No RE match for version code") vercode = m.group(1).strip() version = "??" if len(urlver) > 0: if urlver != '.': logging.debug("...requesting {0}".format(urlver)) req = urllib.request.Request(urlver, None) resp = urllib.request.urlopen(req, None, 20) page = resp.read().decode('utf-8') m = re.search(verex, page) if not m: raise FDroidException("No RE match for version") version = m.group(1) return (version, vercode) except FDroidException: msg = "Could not complete http check for app {0} due to unknown error: {1}".format(app.id, traceback.format_exc()) return (None, msg) # Check for a new version by looking at the tags in the source repo. # Whether this can be used reliably or not depends on # the development procedures used by the project's developers. Use it with # caution, because it's inappropriate for many projects. # Returns (None, "a message") if this didn't work, or (version, vercode, tag) for # the details of the current version. def check_tags(app, pattern): try: if app.RepoType == 'srclib': build_dir = os.path.join('build', 'srclib', app.Repo) repotype = common.getsrclibvcs(app.Repo) else: build_dir = os.path.join('build', app.id) repotype = app.RepoType if repotype not in ('git', 'git-svn', 'hg', 'bzr'): return (None, 'Tags update mode only works for git, hg, bzr and git-svn repositories currently', None) if repotype == 'git-svn' and ';' not in app.Repo: return (None, 'Tags update mode used in git-svn, but the repo was not set up with tags', None) # Set up vcs interface and make sure we have the latest code... vcs = common.getvcs(app.RepoType, app.Repo, build_dir) vcs.gotorevision(None) last_build = app.get_last_build() try_init_submodules(app, last_build, vcs) hpak = None htag = None hver = None hcode = "0" tags = [] if repotype == 'git': tags = vcs.latesttags() else: tags = vcs.gettags() if not tags: return (None, "No tags found", None) logging.debug("All tags: " + ','.join(tags)) if pattern: pat = re.compile(pattern) tags = [tag for tag in tags if pat.match(tag)] if not tags: return (None, "No matching tags found", None) logging.debug("Matching tags: " + ','.join(tags)) if len(tags) > 5 and repotype == 'git': tags = tags[:5] logging.debug("Latest tags: " + ','.join(tags)) for tag in tags: logging.debug("Check tag: '{0}'".format(tag)) vcs.gotorevision(tag) for subdir in possible_subdirs(app): if subdir == '.': root_dir = build_dir else: root_dir = os.path.join(build_dir, subdir) paths = common.manifest_paths(root_dir, last_build.gradle) version, vercode, package = common.parse_androidmanifests(paths, app) if vercode: logging.debug("Manifest exists in subdir '{0}'. Found version {1} ({2})" .format(subdir, version, vercode)) if int(vercode) > int(hcode): hpak = package htag = tag hcode = str(int(vercode)) hver = version if not hpak: return (None, "Couldn't find package ID", None) if hver: return (hver, hcode, htag) return (None, "Couldn't find any version information", None) except VCSException as vcse: msg = "VCS error while scanning app {0}: {1}".format(app.id, vcse) return (None, msg, None) except Exception: msg = "Could not scan app {0} due to unknown error: {1}".format(app.id, traceback.format_exc()) return (None, msg, None) # Check for a new version by looking at the AndroidManifest.xml at the HEAD # of the source repo. Whether this can be used reliably or not depends on # the development procedures used by the project's developers. Use it with # caution, because it's inappropriate for many projects. # Returns (None, "a message") if this didn't work, or (version, vercode) for # the details of the current version. def check_repomanifest(app, branch=None): try: if app.RepoType == 'srclib': build_dir = os.path.join('build', 'srclib', app.Repo) repotype = common.getsrclibvcs(app.Repo) else: build_dir = os.path.join('build', app.id) repotype = app.RepoType # Set up vcs interface and make sure we have the latest code... vcs = common.getvcs(app.RepoType, app.Repo, build_dir) if repotype == 'git': if branch: branch = 'origin/' + branch vcs.gotorevision(branch) elif repotype == 'git-svn': vcs.gotorevision(branch) elif repotype == 'hg': vcs.gotorevision(branch) elif repotype == 'bzr': vcs.gotorevision(None) last_build = metadata.Build() if len(app.builds) > 0: last_build = app.builds[-1] try_init_submodules(app, last_build, vcs) hpak = None hver = None hcode = "0" for subdir in possible_subdirs(app): if subdir == '.': root_dir = build_dir else: root_dir = os.path.join(build_dir, subdir) paths = common.manifest_paths(root_dir, last_build.gradle) version, vercode, package = common.parse_androidmanifests(paths, app) if vercode: logging.debug("Manifest exists in subdir '{0}'. Found version {1} ({2})" .format(subdir, version, vercode)) if int(vercode) > int(hcode): hpak = package hcode = str(int(vercode)) hver = version if not hpak: return (None, "Couldn't find package ID") if hver: return (hver, hcode) return (None, "Couldn't find any version information") except VCSException as vcse: msg = "VCS error while scanning app {0}: {1}".format(app.id, vcse) return (None, msg) except Exception: msg = "Could not scan app {0} due to unknown error: {1}".format(app.id, traceback.format_exc()) return (None, msg) def check_repotrunk(app): try: if app.RepoType == 'srclib': build_dir = os.path.join('build', 'srclib', app.Repo) repotype = common.getsrclibvcs(app.Repo) else: build_dir = os.path.join('build', app.id) repotype = app.RepoType if repotype not in ('git-svn', ): return (None, 'RepoTrunk update mode only makes sense in git-svn repositories') # Set up vcs interface and make sure we have the latest code... vcs = common.getvcs(app.RepoType, app.Repo, build_dir) vcs.gotorevision(None) ref = vcs.getref() return (ref, ref) except VCSException as vcse: msg = "VCS error while scanning app {0}: {1}".format(app.id, vcse) return (None, msg) except Exception: msg = "Could not scan app {0} due to unknown error: {1}".format(app.id, traceback.format_exc()) return (None, msg) # Check for a new version by looking at the Google Play Store. # Returns (None, "a message") if this didn't work, or (version, None) for # the details of the current version. def check_gplay(app): time.sleep(15) url = 'https://play.google.com/store/apps/details?id=' + app.id headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux i686; rv:18.0) Gecko/20100101 Firefox/18.0'} req = urllib.request.Request(url, None, headers) try: resp = urllib.request.urlopen(req, None, 20) page = resp.read().decode() except urllib.error.HTTPError as e: return (None, str(e.code)) except Exception as e: return (None, 'Failed:' + str(e)) version = None m = re.search('itemprop="softwareVersion">[ ]*([^<]+)[ ]*', page) if m: version = html.unescape(m.group(1)) if version == 'Varies with device': return (None, 'Device-variable version, cannot use this method') if not version: return (None, "Couldn't find version") return (version.strip(), None) def try_init_submodules(app, last_build, vcs): """Try to init submodules if the last build entry used them. They might have been removed from the app's repo in the meantime, so if we can't find any submodules we continue with the updates check. If there is any other error in initializing them then we stop the check. """ if last_build.submodules: try: vcs.initsubmodules() except NoSubmodulesException: logging.info("No submodules present for {}".format(app.Name)) # Return all directories under startdir that contain any of the manifest # files, and thus are probably an Android project. def dirs_with_manifest(startdir): for root, dirs, files in os.walk(startdir): if any(m in files for m in [ 'AndroidManifest.xml', 'pom.xml', 'build.gradle']): yield root # Tries to find a new subdir starting from the root build_dir. Returns said # subdir relative to the build dir if found, None otherwise. def possible_subdirs(app): if app.RepoType == 'srclib': build_dir = os.path.join('build', 'srclib', app.Repo) else: build_dir = os.path.join('build', app.id) last_build = app.get_last_build() for d in dirs_with_manifest(build_dir): m_paths = common.manifest_paths(d, last_build.gradle) package = common.parse_androidmanifests(m_paths, app)[2] if package is not None: subdir = os.path.relpath(d, build_dir) logging.debug("Adding possible subdir %s" % subdir) yield subdir def fetch_autoname(app, tag): if not app.RepoType or app.UpdateCheckMode in ('None', 'Static'): return None if app.RepoType == 'srclib': build_dir = os.path.join('build', 'srclib', app.Repo) else: build_dir = os.path.join('build', app.id) try: vcs = common.getvcs(app.RepoType, app.Repo, build_dir) vcs.gotorevision(tag) except VCSException: return None last_build = app.get_last_build() logging.debug("...fetch auto name from " + build_dir) new_name = None for subdir in possible_subdirs(app): if subdir == '.': root_dir = build_dir else: root_dir = os.path.join(build_dir, subdir) new_name = common.fetch_real_name(root_dir, last_build.gradle) if new_name is not None: break commitmsg = None if new_name: logging.debug("...got autoname '" + new_name + "'") if new_name != app.AutoName: app.AutoName = new_name if not commitmsg: commitmsg = "Set autoname of {0}".format(common.getappname(app)) else: logging.debug("...couldn't get autoname") return commitmsg def checkupdates_app(app): # If a change is made, commitmsg should be set to a description of it. # Only if this is set will changes be written back to the metadata. commitmsg = None tag = None msg = None vercode = None noverok = False mode = app.UpdateCheckMode if mode.startswith('Tags'): pattern = mode[5:] if len(mode) > 4 else None (version, vercode, tag) = check_tags(app, pattern) if version == 'Unknown': version = tag msg = vercode elif mode == 'RepoManifest': (version, vercode) = check_repomanifest(app) msg = vercode elif mode.startswith('RepoManifest/'): tag = mode[13:] (version, vercode) = check_repomanifest(app, tag) msg = vercode elif mode == 'RepoTrunk': (version, vercode) = check_repotrunk(app) msg = vercode elif mode == 'HTTP': (version, vercode) = check_http(app) msg = vercode elif mode in ('None', 'Static'): version = None msg = 'Checking disabled' noverok = True else: version = None msg = 'Invalid update check method' if version and vercode and app.VercodeOperation: if not common.VERCODE_OPERATION_RE.match(app.VercodeOperation): raise MetaDataException(_('Invalid VercodeOperation: {field}') .format(field=app.VercodeOperation)) oldvercode = str(int(vercode)) op = app.VercodeOperation.replace("%c", oldvercode) vercode = str(common.calculate_math_string(op)) logging.debug("Applied vercode operation: %s -> %s" % (oldvercode, vercode)) if version and any(version.startswith(s) for s in [ '${', # Gradle variable names '@string/', # Strings we could not resolve ]): version = "Unknown" updating = False if version is None: logmsg = "...{0} : {1}".format(app.id, msg) if noverok: logging.info(logmsg) else: logging.warn(logmsg) elif vercode == app.CurrentVersionCode: logging.info("...up to date") else: logging.debug("...updating - old vercode={0}, new vercode={1}".format( app.CurrentVersionCode, vercode)) app.CurrentVersion = version app.CurrentVersionCode = str(int(vercode)) updating = True commitmsg = fetch_autoname(app, tag) if updating: name = common.getappname(app) ver = common.getcvname(app) logging.info('...updating to version %s' % ver) commitmsg = 'Update CV of %s to %s' % (name, ver) if options.auto: mode = app.AutoUpdateMode if not app.CurrentVersionCode: logging.warn("Can't auto-update app with no current version code: " + app.id) elif mode in ('None', 'Static'): pass elif mode.startswith('Version '): pattern = mode[8:] if pattern.startswith('+'): try: suffix, pattern = pattern.split(' ', 1) except ValueError: raise MetaDataException("Invalid AUM: " + mode) else: suffix = '' gotcur = False latest = None for build in app.builds: if int(build.versionCode) >= int(app.CurrentVersionCode): gotcur = True if not latest or int(build.versionCode) > int(latest.versionCode): latest = build if int(latest.versionCode) > int(app.CurrentVersionCode): logging.info("Refusing to auto update, since the latest build is newer") if not gotcur: newbuild = copy.deepcopy(latest) newbuild.disable = False newbuild.versionCode = app.CurrentVersionCode newbuild.versionName = app.CurrentVersion + suffix logging.info("...auto-generating build for " + newbuild.versionName) commit = pattern.replace('%v', newbuild.versionName) commit = commit.replace('%c', newbuild.versionCode) newbuild.commit = commit app.builds.append(newbuild) name = common.getappname(app) ver = common.getcvname(app) commitmsg = "Update %s to %s" % (name, ver) else: logging.warn('Invalid auto update mode "' + mode + '" on ' + app.id) if commitmsg: metadata.write_metadata(app.metadatapath, app) if options.commit: logging.info("Commiting update for " + app.metadatapath) gitcmd = ["git", "commit", "-m", commitmsg] if 'auto_author' in config: gitcmd.extend(['--author', config['auto_author']]) gitcmd.extend(["--", app.metadatapath]) if subprocess.call(gitcmd) != 0: raise FDroidException("Git commit failed") def update_wiki(gplaylog, locallog): if config.get('wiki_server') and config.get('wiki_path'): try: import mwclient site = mwclient.Site((config['wiki_protocol'], config['wiki_server']), path=config['wiki_path']) site.login(config['wiki_user'], config['wiki_password']) # Write a page with the last build log for this version code wiki_page_path = 'checkupdates_' + time.strftime('%s', start_timestamp) newpage = site.Pages[wiki_page_path] txt = '' txt += "* command line: " + ' '.join(sys.argv) + "\n" txt += common.get_git_describe_link() txt += "* started at " + common.get_wiki_timestamp(start_timestamp) + '\n' txt += "* completed at " + common.get_wiki_timestamp() + '\n' txt += "\n\n" txt += common.get_android_tools_version_log() txt += "\n\n" if gplaylog: txt += '== --gplay check ==\n\n' txt += gplaylog if locallog: txt += '== local source check ==\n\n' txt += locallog newpage.save(txt, summary='Run log') newpage = site.Pages['checkupdates'] newpage.save('#REDIRECT [[' + wiki_page_path + ']]', summary='Update redirect') except Exception as e: logging.error(_('Error while attempting to publish log: %s') % e) config = None options = None start_timestamp = time.gmtime() def main(): global config, options # Parse command line... parser = ArgumentParser(usage="%(prog)s [options] [APPID [APPID ...]]") common.setup_global_opts(parser) parser.add_argument("appid", nargs='*', help=_("applicationId to check for updates")) parser.add_argument("--auto", action="store_true", default=False, help=_("Process auto-updates")) parser.add_argument("--autoonly", action="store_true", default=False, help=_("Only process apps with auto-updates")) parser.add_argument("--commit", action="store_true", default=False, help=_("Commit changes")) parser.add_argument("--allow-dirty", action="store_true", default=False, help=_("Run on git repo that has uncommitted changes")) parser.add_argument("--gplay", action="store_true", default=False, help=_("Only print differences with the Play Store")) metadata.add_metadata_arguments(parser) options = parser.parse_args() metadata.warnings_action = options.W config = common.read_config(options) if not options.allow_dirty: status = subprocess.check_output(['git', 'status', '--porcelain']) if status: logging.error(_('Build metadata git repo has uncommited changes!')) sys.exit(1) # Get all apps... allapps = metadata.read_metadata() apps = common.read_app_args(options.appid, allapps, False) gplaylog = '' if options.gplay: for appid, app in apps.items(): gplaylog += '* ' + appid + '\n' version, reason = check_gplay(app) if version is None: if reason == '404': logging.info("{0} is not in the Play Store".format(common.getappname(app))) else: logging.info("{0} encountered a problem: {1}".format(common.getappname(app), reason)) if version is not None: stored = app.CurrentVersion if not stored: logging.info("{0} has no Current Version but has version {1} on the Play Store" .format(common.getappname(app), version)) elif LooseVersion(stored) < LooseVersion(version): logging.info("{0} has version {1} on the Play Store, which is bigger than {2}" .format(common.getappname(app), version, stored)) else: if stored != version: logging.info("{0} has version {1} on the Play Store, which differs from {2}" .format(common.getappname(app), version, stored)) else: logging.info("{0} has the same version {1} on the Play Store" .format(common.getappname(app), version)) update_wiki(gplaylog, None) return locallog = '' for appid, app in apps.items(): if options.autoonly and app.AutoUpdateMode in ('None', 'Static'): logging.debug(_("Nothing to do for {appid}.").format(appid=appid)) continue msg = _("Processing {appid}").format(appid=appid) logging.info(msg) locallog += '* ' + msg + '\n' try: checkupdates_app(app) except Exception as e: msg = _("...checkupdate failed for {appid} : {error}").format(appid=appid, error=e) logging.error(msg) locallog += msg + '\n' update_wiki(None, locallog) logging.info(_("Finished")) if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/common.py0000644000175000017500000040302613576156546020510 0ustar hanshans00000000000000#!/usr/bin/env python3 # # common.py - part of the FDroid server tools # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # # 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 . # common.py is imported by all modules, so do not import third-party # libraries here as they will become a requirement for all commands. import io import os import sys import re import ast import gzip import shutil import glob import stat import subprocess import time import operator import logging import hashlib import socket import base64 import zipfile import tempfile import json # TODO change to only import defusedxml once its installed everywhere try: import defusedxml.ElementTree as XMLElementTree except ImportError: import xml.etree.ElementTree as XMLElementTree # nosec this is a fallback only from binascii import hexlify from datetime import datetime, timedelta from distutils.version import LooseVersion from queue import Queue from zipfile import ZipFile from pyasn1.codec.der import decoder, encoder from pyasn1_modules import rfc2315 from pyasn1.error import PyAsn1Error import fdroidserver.metadata from fdroidserver import _ from fdroidserver.exception import FDroidException, VCSException, NoSubmodulesException,\ BuildException, VerificationException from .asynchronousfilereader import AsynchronousFileReader # The path to this fdroidserver distribution FDROID_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) # this is the build-tools version, aapt has a separate version that # has to be manually set in test_aapt_version() MINIMUM_AAPT_VERSION = '26.0.0' VERCODE_OPERATION_RE = re.compile(r'^([ 0-9/*+-]|%c)+$') # A signature block file with a .DSA, .RSA, or .EC extension SIGNATURE_BLOCK_FILE_REGEX = re.compile(r'^META-INF/.*\.(DSA|EC|RSA)$') APK_NAME_REGEX = re.compile(r'^([a-zA-Z][\w.]*)_(-?[0-9]+)_?([0-9a-f]{7})?\.apk') APK_ID_TRIPLET_REGEX = re.compile(r"^package: name='(\w[^']*)' versionCode='([^']+)' versionName='([^']*)'") STANDARD_FILE_NAME_REGEX = re.compile(r'^(\w[\w.]*)_(-?[0-9]+)\.\w+') FDROID_PACKAGE_NAME_REGEX = re.compile(r'''^[a-f0-9]+$''', re.IGNORECASE) STRICT_APPLICATION_ID_REGEX = re.compile(r'''(?:^[a-zA-Z]+(?:\d*[a-zA-Z_]*)*)(?:\.[a-zA-Z]+(?:\d*[a-zA-Z_]*)*)+$''') VALID_APPLICATION_ID_REGEX = re.compile(r'''(?:^[a-z_]+(?:\d*[a-zA-Z_]*)*)(?:\.[a-z_]+(?:\d*[a-zA-Z_]*)*)*$''', re.IGNORECASE) MAX_VERSION_CODE = 0x7fffffff # Java's Integer.MAX_VALUE (2147483647) XMLNS_ANDROID = '{http://schemas.android.com/apk/res/android}' config = None options = None env = None orig_path = None default_config = { 'sdk_path': "$ANDROID_HOME", 'ndk_paths': { 'r10e': None, 'r11c': None, 'r12b': "$ANDROID_NDK", 'r13b': None, 'r14b': None, 'r15c': None, 'r16b': None, }, 'cachedir': os.path.join(os.getenv('HOME'), '.cache', 'fdroidserver'), 'build_tools': MINIMUM_AAPT_VERSION, 'force_build_tools': False, 'java_paths': None, 'ant': "ant", 'mvn3': "mvn", 'gradle': os.path.join(FDROID_PATH, 'gradlew-fdroid'), 'gradle_version_dir': os.path.join(os.path.join(os.getenv('HOME'), '.cache', 'fdroidserver'), 'gradle'), 'accepted_formats': ['txt', 'yml'], 'sync_from_local_copy_dir': False, 'allow_disabled_algorithms': False, 'per_app_repos': False, 'make_current_version_link': True, 'current_version_name_source': 'Name', 'deploy_process_logs': False, 'update_stats': False, 'stats_ignore': [], 'stats_server': None, 'stats_user': None, 'stats_to_carbon': False, 'repo_maxage': 0, 'build_server_always': False, 'keystore': 'keystore.jks', 'smartcardoptions': [], 'char_limits': { 'author': 256, 'name': 30, 'summary': 80, 'description': 4000, 'video': 256, 'whatsNew': 500, }, 'keyaliases': {}, 'repo_url': "https://MyFirstFDroidRepo.org/fdroid/repo", 'repo_name': "My First FDroid Repo Demo", 'repo_icon': "fdroid-icon.png", 'repo_description': ''' This is a repository of apps to be used with FDroid. Applications in this repository are either official binaries built by the original application developers, or are binaries built from source by the admin of f-droid.org using the tools on https://gitlab.com/u/fdroid. ''', 'archive_older': 0, } def setup_global_opts(parser): try: # the buildserver VM might not have PIL installed from PIL import PngImagePlugin logger = logging.getLogger(PngImagePlugin.__name__) logger.setLevel(logging.INFO) # tame the "STREAM" debug messages except ImportError: pass parser.add_argument("-v", "--verbose", action="store_true", default=False, help=_("Spew out even more information than normal")) parser.add_argument("-q", "--quiet", action="store_true", default=False, help=_("Restrict output to warnings and errors")) def _add_java_paths_to_config(pathlist, thisconfig): def path_version_key(s): versionlist = [] for u in re.split('[^0-9]+', s): try: versionlist.append(int(u)) except ValueError: pass return versionlist for d in sorted(pathlist, key=path_version_key): if os.path.islink(d): continue j = os.path.basename(d) # the last one found will be the canonical one, so order appropriately for regex in [ r'^1\.([16-9][0-9]?)\.0\.jdk$', # OSX r'^jdk1\.([16-9][0-9]?)\.0_[0-9]+.jdk$', # OSX and Oracle tarball r'^jdk1\.([16-9][0-9]?)\.0_[0-9]+$', # Oracle Windows r'^jdk([16-9][0-9]?)-openjdk$', # Arch r'^java-([16-9][0-9]?)-openjdk$', # Arch r'^java-([16-9][0-9]?)-jdk$', # Arch (oracle) r'^java-1\.([16-9][0-9]?)\.0-.*$', # RedHat r'^java-([16-9][0-9]?)-oracle$', # Debian WebUpd8 r'^jdk-([16-9][0-9]?)-oracle-.*$', # Debian make-jpkg r'^java-([16-9][0-9]?)-openjdk-[^c][^o][^m].*$', # Debian r'^oracle-jdk-bin-1\.([17-9][0-9]?).*$', # Gentoo (oracle) r'^icedtea-bin-([17-9][0-9]?).*$', # Gentoo (openjdk) ]: m = re.match(regex, j) if not m: continue for p in [d, os.path.join(d, 'Contents', 'Home')]: if os.path.exists(os.path.join(p, 'bin', 'javac')): thisconfig['java_paths'][m.group(1)] = p def fill_config_defaults(thisconfig): for k, v in default_config.items(): if k not in thisconfig: thisconfig[k] = v # Expand paths (~users and $vars) def expand_path(path): if path is None: return None orig = path path = os.path.expanduser(path) path = os.path.expandvars(path) if orig == path: return None return path for k in ['sdk_path', 'ant', 'mvn3', 'gradle', 'keystore', 'repo_icon']: v = thisconfig[k] exp = expand_path(v) if exp is not None: thisconfig[k] = exp thisconfig[k + '_orig'] = v # find all installed JDKs for keytool, jarsigner, and JAVA[6-9]_HOME env vars if thisconfig['java_paths'] is None: thisconfig['java_paths'] = dict() pathlist = [] pathlist += glob.glob('/usr/lib/jvm/j*[16-9]*') pathlist += glob.glob('/usr/java/jdk1.[16-9]*') pathlist += glob.glob('/System/Library/Java/JavaVirtualMachines/1.[16-9][0-9]?.0.jdk') pathlist += glob.glob('/Library/Java/JavaVirtualMachines/*jdk*[0-9]*') pathlist += glob.glob('/opt/oracle-jdk-*1.[0-9]*') pathlist += glob.glob('/opt/icedtea-*[0-9]*') if os.getenv('JAVA_HOME') is not None: pathlist.append(os.getenv('JAVA_HOME')) if os.getenv('PROGRAMFILES') is not None: pathlist += glob.glob(os.path.join(os.getenv('PROGRAMFILES'), 'Java', 'jdk1.[16-9][0-9]?.*')) _add_java_paths_to_config(pathlist, thisconfig) for java_version in ('14', '13', '12', '11', '10', '9', '8', '7'): if java_version not in thisconfig['java_paths']: continue java_home = thisconfig['java_paths'][java_version] jarsigner = os.path.join(java_home, 'bin', 'jarsigner') if os.path.exists(jarsigner): thisconfig['jarsigner'] = jarsigner thisconfig['keytool'] = os.path.join(java_home, 'bin', 'keytool') break if 'jarsigner' not in thisconfig and shutil.which('jarsigner'): thisconfig['jarsigner'] = shutil.which('jarsigner') if 'keytool' not in thisconfig and shutil.which('keytool'): thisconfig['keytool'] = shutil.which('keytool') for k in ['ndk_paths', 'java_paths']: d = thisconfig[k] for k2 in d.copy(): v = d[k2] exp = expand_path(v) if exp is not None: thisconfig[k][k2] = exp thisconfig[k][k2 + '_orig'] = v def regsub_file(pattern, repl, path): with open(path, 'rb') as f: text = f.read() text = re.sub(bytes(pattern, 'utf8'), bytes(repl, 'utf8'), text) with open(path, 'wb') as f: f.write(text) def read_config(opts, config_file='config.py'): """Read the repository config The config is read from config_file, which is in the current directory when any of the repo management commands are used. If there is a local metadata file in the git repo, then config.py is not required, just use defaults. """ global config, options if config is not None: return config options = opts config = {} if os.path.isfile(config_file): logging.debug(_("Reading '{config_file}'").format(config_file=config_file)) with io.open(config_file, "rb") as f: code = compile(f.read(), config_file, 'exec') exec(code, None, config) # nosec TODO switch to YAML file else: logging.warning(_("No 'config.py' found, using defaults.")) for k in ('mirrors', 'install_list', 'uninstall_list', 'serverwebroot', 'servergitroot'): if k in config: if not type(config[k]) in (str, list, tuple): logging.warning( _("'{field}' will be in random order! Use () or [] brackets if order is important!") .format(field=k)) # smartcardoptions must be a list since its command line args for Popen if 'smartcardoptions' in config: config['smartcardoptions'] = config['smartcardoptions'].split(' ') elif 'keystore' in config and config['keystore'] == 'NONE': # keystore='NONE' means use smartcard, these are required defaults config['smartcardoptions'] = ['-storetype', 'PKCS11', '-providerName', 'SunPKCS11-OpenSC', '-providerClass', 'sun.security.pkcs11.SunPKCS11', '-providerArg', 'opensc-fdroid.cfg'] if any(k in config for k in ["keystore", "keystorepass", "keypass"]): st = os.stat(config_file) if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO: logging.warning(_("unsafe permissions on '{config_file}' (should be 0600)!") .format(config_file=config_file)) fill_config_defaults(config) for k in ["repo_description", "archive_description"]: if k in config: config[k] = clean_description(config[k]) if 'serverwebroot' in config: if isinstance(config['serverwebroot'], str): roots = [config['serverwebroot']] elif all(isinstance(item, str) for item in config['serverwebroot']): roots = config['serverwebroot'] else: raise TypeError(_('only accepts strings, lists, and tuples')) rootlist = [] for rootstr in roots: # since this is used with rsync, where trailing slashes have # meaning, ensure there is always a trailing slash if rootstr[-1] != '/': rootstr += '/' rootlist.append(rootstr.replace('//', '/')) config['serverwebroot'] = rootlist if 'servergitmirrors' in config: if isinstance(config['servergitmirrors'], str): roots = [config['servergitmirrors']] elif all(isinstance(item, str) for item in config['servergitmirrors']): roots = config['servergitmirrors'] else: raise TypeError(_('only accepts strings, lists, and tuples')) config['servergitmirrors'] = roots return config def assert_config_keystore(config): """Check weather keystore is configured correctly and raise exception if not.""" nosigningkey = False if 'repo_keyalias' not in config: nosigningkey = True logging.critical(_("'repo_keyalias' not found in config.py!")) if 'keystore' not in config: nosigningkey = True logging.critical(_("'keystore' not found in config.py!")) elif not os.path.exists(config['keystore']): nosigningkey = True logging.critical("'" + config['keystore'] + "' does not exist!") if 'keystorepass' not in config: nosigningkey = True logging.critical(_("'keystorepass' not found in config.py!")) if 'keypass' not in config: nosigningkey = True logging.critical(_("'keypass' not found in config.py!")) if nosigningkey: raise FDroidException("This command requires a signing key, " + "you can create one using: fdroid update --create-key") def find_sdk_tools_cmd(cmd): '''find a working path to a tool from the Android SDK''' tooldirs = [] if config is not None and 'sdk_path' in config and os.path.exists(config['sdk_path']): # try to find a working path to this command, in all the recent possible paths if 'build_tools' in config: build_tools = os.path.join(config['sdk_path'], 'build-tools') # if 'build_tools' was manually set and exists, check only that one configed_build_tools = os.path.join(build_tools, config['build_tools']) if os.path.exists(configed_build_tools): tooldirs.append(configed_build_tools) else: # no configed version, so hunt known paths for it for f in sorted(os.listdir(build_tools), reverse=True): if os.path.isdir(os.path.join(build_tools, f)): tooldirs.append(os.path.join(build_tools, f)) tooldirs.append(build_tools) sdk_tools = os.path.join(config['sdk_path'], 'tools') if os.path.exists(sdk_tools): tooldirs.append(sdk_tools) sdk_platform_tools = os.path.join(config['sdk_path'], 'platform-tools') if os.path.exists(sdk_platform_tools): tooldirs.append(sdk_platform_tools) tooldirs.append('/usr/bin') for d in tooldirs: path = os.path.join(d, cmd) if os.path.isfile(path): if cmd == 'aapt': test_aapt_version(path) return path # did not find the command, exit with error message ensure_build_tools_exists(config) def test_aapt_version(aapt): '''Check whether the version of aapt is new enough''' output = subprocess.check_output([aapt, 'version'], universal_newlines=True) if output is None or output == '': logging.error(_("'{path}' failed to execute!").format(path=aapt)) else: m = re.match(r'.*v([0-9]+)\.([0-9]+)[.-]?([0-9.-]*)', output) if m: major = m.group(1) minor = m.group(2) bugfix = m.group(3) # the Debian package has the version string like "v0.2-23.0.2" too_old = False if '.' in bugfix: if LooseVersion(bugfix) < LooseVersion(MINIMUM_AAPT_VERSION): too_old = True elif LooseVersion('.'.join((major, minor, bugfix))) < LooseVersion('0.2.4062713'): too_old = True if too_old: logging.warning(_("'{aapt}' is too old, fdroid requires build-tools-{version} or newer!") .format(aapt=aapt, version=MINIMUM_AAPT_VERSION)) else: logging.warning(_('Unknown version of aapt, might cause problems: ') + output) def test_sdk_exists(thisconfig): if 'sdk_path' not in thisconfig: if 'aapt' in thisconfig and os.path.isfile(thisconfig['aapt']): test_aapt_version(thisconfig['aapt']) return True else: logging.error(_("'sdk_path' not set in 'config.py'!")) return False if thisconfig['sdk_path'] == default_config['sdk_path']: logging.error(_('No Android SDK found!')) logging.error(_('You can use ANDROID_HOME to set the path to your SDK, i.e.:')) logging.error('\texport ANDROID_HOME=/opt/android-sdk') return False if not os.path.exists(thisconfig['sdk_path']): logging.critical(_("Android SDK path '{path}' does not exist!") .format(path=thisconfig['sdk_path'])) return False if not os.path.isdir(thisconfig['sdk_path']): logging.critical(_("Android SDK path '{path}' is not a directory!") .format(path=thisconfig['sdk_path'])) return False return True def ensure_build_tools_exists(thisconfig): if not test_sdk_exists(thisconfig): raise FDroidException(_("Android SDK not found!")) build_tools = os.path.join(thisconfig['sdk_path'], 'build-tools') versioned_build_tools = os.path.join(build_tools, thisconfig['build_tools']) if not os.path.isdir(versioned_build_tools): raise FDroidException( _("Android build-tools path '{path}' does not exist!") .format(path=versioned_build_tools)) def get_local_metadata_files(): '''get any metadata files local to an app's source repo This tries to ignore anything that does not count as app metdata, including emacs cruft ending in ~ and the .fdroid.key*pass.txt files. ''' return glob.glob('.fdroid.[a-jl-z]*[a-rt-z]') def read_pkg_args(appid_versionCode_pairs, allow_vercodes=False): """ :param appids: arguments in the form of multiple appid:[vc] strings :returns: a dictionary with the set of vercodes specified for each package """ vercodes = {} if not appid_versionCode_pairs: return vercodes for p in appid_versionCode_pairs: if allow_vercodes and ':' in p: package, vercode = p.split(':') else: package, vercode = p, None if package not in vercodes: vercodes[package] = [vercode] if vercode else [] continue elif vercode and vercode not in vercodes[package]: vercodes[package] += [vercode] if vercode else [] return vercodes def read_app_args(appid_versionCode_pairs, allapps, allow_vercodes=False): """Build a list of App instances for processing On top of what read_pkg_args does, this returns the whole app metadata, but limiting the builds list to the builds matching the appid_versionCode_pairs and vercodes specified. If no appid_versionCode_pairs are specified, then all App and Build instances are returned. """ vercodes = read_pkg_args(appid_versionCode_pairs, allow_vercodes) if not vercodes: return allapps apps = {} for appid, app in allapps.items(): if appid in vercodes: apps[appid] = app if len(apps) != len(vercodes): for p in vercodes: if p not in allapps: logging.critical(_("No such package: %s") % p) raise FDroidException(_("Found invalid appids in arguments")) if not apps: raise FDroidException(_("No packages specified")) error = False for appid, app in apps.items(): vc = vercodes[appid] if not vc: continue app.builds = [b for b in app.builds if b.versionCode in vc] if len(app.builds) != len(vercodes[appid]): error = True allvcs = [b.versionCode for b in app.builds] for v in vercodes[appid]: if v not in allvcs: logging.critical(_("No such versionCode {versionCode} for app {appid}") .format(versionCode=v, appid=appid)) if error: raise FDroidException(_("Found invalid versionCodes for some apps")) return apps def get_extension(filename): base, ext = os.path.splitext(filename) if not ext: return base, '' return base, ext.lower()[1:] def has_extension(filename, ext): _ignored, f_ext = get_extension(filename) return ext == f_ext publish_name_regex = re.compile(r"^(.+)_([0-9]+)\.(apk|zip)$") def clean_description(description): 'Remove unneeded newlines and spaces from a block of description text' returnstring = '' # this is split up by paragraph to make removing the newlines easier for paragraph in re.split(r'\n\n', description): paragraph = re.sub('\r', '', paragraph) paragraph = re.sub('\n', ' ', paragraph) paragraph = re.sub(' {2,}', ' ', paragraph) paragraph = re.sub(r'^\s*(\w)', r'\1', paragraph) returnstring += paragraph + '\n\n' return returnstring.rstrip('\n') def publishednameinfo(filename): filename = os.path.basename(filename) m = publish_name_regex.match(filename) try: result = (m.group(1), m.group(2)) except AttributeError: raise FDroidException(_("Invalid name for published file: %s") % filename) return result apk_release_filename = re.compile(r'(?P[a-zA-Z0-9_\.]+)_(?P[0-9]+)\.apk') apk_release_filename_with_sigfp = re.compile(r'(?P[a-zA-Z0-9_\.]+)_(?P[0-9]+)_(?P[0-9a-f]{7})\.apk') def apk_parse_release_filename(apkname): """Parses the name of an APK file according the F-Droids APK naming scheme and returns the tokens. WARNING: Returned values don't necessarily represent the APKs actual properties, the are just paresed from the file name. :returns: A triplet containing (appid, versionCode, signer), where appid should be the package name, versionCode should be the integer represion of the APKs version and signer should be the first 7 hex digists of the sha256 signing key fingerprint which was used to sign this APK. """ m = apk_release_filename_with_sigfp.match(apkname) if m: return m.group('appid'), m.group('vercode'), m.group('sigfp') m = apk_release_filename.match(apkname) if m: return m.group('appid'), m.group('vercode'), None return None, None, None def get_release_filename(app, build): if build.output: return "%s_%s.%s" % (app.id, build.versionCode, get_file_extension(build.output)) else: return "%s_%s.apk" % (app.id, build.versionCode) def get_toolsversion_logname(app, build): return "%s_%s_toolsversion.log" % (app.id, build.versionCode) def getsrcname(app, build): return "%s_%s_src.tar.gz" % (app.id, build.versionCode) def getappname(app): if app.Name: return app.Name if app.AutoName: return app.AutoName return app.id def getcvname(app): return '%s (%s)' % (app.CurrentVersion, app.CurrentVersionCode) def get_build_dir(app): '''get the dir that this app will be built in''' if app.RepoType == 'srclib': return os.path.join('build', 'srclib', app.Repo) return os.path.join('build', app.id) def setup_vcs(app): '''checkout code from VCS and return instance of vcs and the build dir''' build_dir = get_build_dir(app) # Set up vcs interface and make sure we have the latest code... logging.debug("Getting {0} vcs interface for {1}" .format(app.RepoType, app.Repo)) if app.RepoType == 'git' and os.path.exists('.fdroid.yml'): remote = os.getcwd() else: remote = app.Repo vcs = getvcs(app.RepoType, remote, build_dir) return vcs, build_dir def getvcs(vcstype, remote, local): if vcstype == 'git': return vcs_git(remote, local) if vcstype == 'git-svn': return vcs_gitsvn(remote, local) if vcstype == 'hg': return vcs_hg(remote, local) if vcstype == 'bzr': return vcs_bzr(remote, local) if vcstype == 'srclib': if local != os.path.join('build', 'srclib', remote): raise VCSException("Error: srclib paths are hard-coded!") return getsrclib(remote, os.path.join('build', 'srclib'), raw=True) if vcstype == 'svn': raise VCSException("Deprecated vcs type 'svn' - please use 'git-svn' instead") raise VCSException("Invalid vcs type " + vcstype) def getsrclibvcs(name): if name not in fdroidserver.metadata.srclibs: raise VCSException("Missing srclib " + name) return fdroidserver.metadata.srclibs[name]['Repo Type'] class vcs: def __init__(self, remote, local): # svn, git-svn and bzr may require auth self.username = None if self.repotype() in ('git-svn', 'bzr'): if '@' in remote: if self.repotype == 'git-svn': raise VCSException("Authentication is not supported for git-svn") self.username, remote = remote.split('@') if ':' not in self.username: raise VCSException(_("Password required with username")) self.username, self.password = self.username.split(':') self.remote = remote self.local = local self.clone_failed = False self.refreshed = False self.srclib = None def repotype(self): return None def clientversion(self): versionstr = FDroidPopen(self.clientversioncmd()).output return versionstr[0:versionstr.find('\n')] def clientversioncmd(self): return None def gotorevision(self, rev, refresh=True): """Take the local repository to a clean version of the given revision, which is specificed in the VCS's native format. Beforehand, the repository can be dirty, or even non-existent. If the repository does already exist locally, it will be updated from the origin, but only once in the lifetime of the vcs object. None is acceptable for 'rev' if you know you are cloning a clean copy of the repo - otherwise it must specify a valid revision. """ if self.clone_failed: raise VCSException(_("Downloading the repository already failed once, not trying again.")) # The .fdroidvcs-id file for a repo tells us what VCS type # and remote that directory was created from, allowing us to drop it # automatically if either of those things changes. fdpath = os.path.join(self.local, '..', '.fdroidvcs-' + os.path.basename(self.local)) fdpath = os.path.normpath(fdpath) cdata = self.repotype() + ' ' + self.remote writeback = True deleterepo = False if os.path.exists(self.local): if os.path.exists(fdpath): with open(fdpath, 'r') as f: fsdata = f.read().strip() if fsdata == cdata: writeback = False else: deleterepo = True logging.info("Repository details for %s changed - deleting" % ( self.local)) else: deleterepo = True logging.info("Repository details for %s missing - deleting" % ( self.local)) if deleterepo: shutil.rmtree(self.local) exc = None if not refresh: self.refreshed = True try: self.gotorevisionx(rev) except FDroidException as e: exc = e # If necessary, write the .fdroidvcs file. if writeback and not self.clone_failed: os.makedirs(os.path.dirname(fdpath), exist_ok=True) with open(fdpath, 'w+') as f: f.write(cdata) if exc is not None: raise exc def gotorevisionx(self, rev): # pylint: disable=unused-argument """Derived classes need to implement this. It's called once basic checking has been performed. """ raise VCSException("This VCS type doesn't define gotorevisionx") # Initialise and update submodules def initsubmodules(self): raise VCSException('Submodules not supported for this vcs type') # Get a list of all known tags def gettags(self): if not self._gettags: raise VCSException('gettags not supported for this vcs type') rtags = [] for tag in self._gettags(): if re.match('[-A-Za-z0-9_. /]+$', tag): rtags.append(tag) return rtags def latesttags(self): """Get a list of all the known tags, sorted from newest to oldest""" raise VCSException('latesttags not supported for this vcs type') def getref(self): """Get current commit reference (hash, revision, etc)""" raise VCSException('getref not supported for this vcs type') def getsrclib(self): """Returns the srclib (name, path) used in setting up the current revision, or None.""" return self.srclib class vcs_git(vcs): def repotype(self): return 'git' def clientversioncmd(self): return ['git', '--version'] def git(self, args, envs=dict(), cwd=None, output=True): '''Prevent git fetch/clone/submodule from hanging at the username/password prompt While fetch/pull/clone respect the command line option flags, it seems that submodule commands do not. They do seem to follow whatever is in env vars, if the version of git is new enough. So we just throw the kitchen sink at it to see what sticks. Also, because of CVE-2017-1000117, block all SSH URLs. ''' # # supported in git >= 2.3 git_config = [ '-c', 'core.askpass=/bin/true', '-c', 'core.sshCommand=/bin/false', '-c', 'url.https://.insteadOf=ssh://', ] for domain in ('bitbucket.org', 'github.com', 'gitlab.com'): git_config.append('-c') git_config.append('url.https://u:p@' + domain + '/.insteadOf=git@' + domain + ':') git_config.append('-c') git_config.append('url.https://u:p@' + domain + '.insteadOf=git://' + domain) git_config.append('-c') git_config.append('url.https://u:p@' + domain + '.insteadOf=https://' + domain) envs.update({ 'GIT_TERMINAL_PROMPT': '0', 'GIT_ASKPASS': '/bin/true', 'SSH_ASKPASS': '/bin/true', 'GIT_SSH': '/bin/false', # for git < 2.3 }) return FDroidPopen(['git', ] + git_config + args, envs=envs, cwd=cwd, output=output) def checkrepo(self): """If the local directory exists, but is somehow not a git repository, git will traverse up the directory tree until it finds one that is (i.e. fdroidserver) and then we'll proceed to destroy it! This is called as a safety check. """ p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False) result = p.output.rstrip() if not result.endswith(self.local): raise VCSException('Repository mismatch') def gotorevisionx(self, rev): if not os.path.exists(self.local): # Brand new checkout p = self.git(['clone', '--', self.remote, self.local]) if p.returncode != 0: self.clone_failed = True raise VCSException("Git clone failed", p.output) self.checkrepo() else: self.checkrepo() # Discard any working tree changes p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive', 'git', 'reset', '--hard'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git reset failed"), p.output) # Remove untracked files now, in case they're tracked in the target # revision (it happens!) p = FDroidPopen(['git', 'submodule', 'foreach', '--recursive', 'git', 'clean', '-dffx'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git clean failed"), p.output) if not self.refreshed: # Get latest commits and tags from remote p = self.git(['fetch', 'origin'], cwd=self.local) if p.returncode != 0: raise VCSException(_("Git fetch failed"), p.output) p = self.git(['fetch', '--prune', '--tags', 'origin'], output=False, cwd=self.local) if p.returncode != 0: raise VCSException(_("Git fetch failed"), p.output) # Recreate origin/HEAD as git clone would do it, in case it disappeared p = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--auto'], cwd=self.local, output=False) if p.returncode != 0: lines = p.output.splitlines() if 'Multiple remote HEAD branches' not in lines[0]: raise VCSException(_("Git remote set-head failed"), p.output) branch = lines[1].split(' ')[-1] p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--', branch], cwd=self.local, output=False) if p2.returncode != 0: raise VCSException(_("Git remote set-head failed"), p.output + '\n' + p2.output) self.refreshed = True # origin/HEAD is the HEAD of the remote, e.g. the "default branch" on # a github repo. Most of the time this is the same as origin/master. rev = rev or 'origin/HEAD' p = FDroidPopen(['git', 'checkout', '-f', rev], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git checkout of '%s' failed") % rev, p.output) # Get rid of any uncontrolled files left behind p = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git clean failed"), p.output) def initsubmodules(self): self.checkrepo() submfile = os.path.join(self.local, '.gitmodules') if not os.path.isfile(submfile): raise NoSubmodulesException(_("No git submodules available")) # fix submodules not accessible without an account and public key auth with open(submfile, 'r') as f: lines = f.readlines() with open(submfile, 'w') as f: for line in lines: for domain in ('bitbucket.org', 'github.com', 'gitlab.com'): line = re.sub('git@' + domain + ':', 'https://u:p@' + domain + '/', line) f.write(line) p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git submodule sync failed"), p.output) p = self.git(['submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local) if p.returncode != 0: raise VCSException(_("Git submodule update failed"), p.output) def _gettags(self): self.checkrepo() p = FDroidPopen(['git', 'tag'], cwd=self.local, output=False) return p.output.splitlines() tag_format = re.compile(r'tag: ([^),]*)') def latesttags(self): self.checkrepo() p = FDroidPopen(['git', 'log', '--tags', '--simplify-by-decoration', '--pretty=format:%d'], cwd=self.local, output=False) tags = [] for line in p.output.splitlines(): for tag in self.tag_format.findall(line): tags.append(tag) return tags class vcs_gitsvn(vcs): def repotype(self): return 'git-svn' def clientversioncmd(self): return ['git', 'svn', '--version'] def checkrepo(self): """If the local directory exists, but is somehow not a git repository, git will traverse up the directory tree until it finds one that is (i.e. fdroidserver) and then we'll proceed to destory it! This is called as a safety check. """ p = FDroidPopen(['git', 'rev-parse', '--show-toplevel'], cwd=self.local, output=False) result = p.output.rstrip() if not result.endswith(self.local): raise VCSException('Repository mismatch') def git(self, args, envs=dict(), cwd=None, output=True): '''Prevent git fetch/clone/submodule from hanging at the username/password prompt AskPass is set to /bin/true to let the process try to connect without a username/password. The SSH command is set to /bin/false to block all SSH URLs (supported in git >= 2.3). This protects against CVE-2017-1000117. ''' git_config = [ '-c', 'core.askpass=/bin/true', '-c', 'core.sshCommand=/bin/false', ] envs.update({ 'GIT_TERMINAL_PROMPT': '0', 'GIT_ASKPASS': '/bin/true', 'SSH_ASKPASS': '/bin/true', 'GIT_SSH': '/bin/false', # for git < 2.3 'SVN_SSH': '/bin/false', }) return FDroidPopen(['git', ] + git_config + args, envs=envs, cwd=cwd, output=output) def gotorevisionx(self, rev): if not os.path.exists(self.local): # Brand new checkout gitsvn_args = ['svn', 'clone'] remote = None if ';' in self.remote: remote_split = self.remote.split(';') for i in remote_split[1:]: if i.startswith('trunk='): gitsvn_args.extend(['-T', i[6:]]) elif i.startswith('tags='): gitsvn_args.extend(['-t', i[5:]]) elif i.startswith('branches='): gitsvn_args.extend(['-b', i[9:]]) remote = remote_split[0] else: remote = self.remote if not remote.startswith('https://'): raise VCSException(_('HTTPS must be used with Subversion URLs!')) # git-svn sucks at certificate validation, this throws useful errors: import requests r = requests.head(remote) r.raise_for_status() location = r.headers.get('location') if location and not location.startswith('https://'): raise VCSException(_('Invalid redirect to non-HTTPS: {before} -> {after} ') .format(before=remote, after=location)) gitsvn_args.extend(['--', remote, self.local]) p = self.git(gitsvn_args) if p.returncode != 0: self.clone_failed = True raise VCSException(_('git svn clone failed'), p.output) self.checkrepo() else: self.checkrepo() # Discard any working tree changes p = self.git(['reset', '--hard'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Git reset failed", p.output) # Remove untracked files now, in case they're tracked in the target # revision (it happens!) p = self.git(['clean', '-dffx'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Git clean failed", p.output) if not self.refreshed: # Get new commits, branches and tags from repo p = self.git(['svn', 'fetch'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Git svn fetch failed") p = self.git(['svn', 'rebase'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Git svn rebase failed", p.output) self.refreshed = True rev = rev or 'master' if rev: nospaces_rev = rev.replace(' ', '%20') # Try finding a svn tag for treeish in ['origin/', '']: p = self.git(['checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False) if p.returncode == 0: break if p.returncode != 0: # No tag found, normal svn rev translation # Translate svn rev into git format rev_split = rev.split('/') p = None for treeish in ['origin/', '']: if len(rev_split) > 1: treeish += rev_split[0] svn_rev = rev_split[1] else: # if no branch is specified, then assume trunk (i.e. 'master' branch): treeish += 'master' svn_rev = rev svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev p = self.git(['svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False) git_rev = p.output.rstrip() if p.returncode == 0 and git_rev: break if p.returncode != 0 or not git_rev: # Try a plain git checkout as a last resort p = self.git(['checkout', rev], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("No git treeish found and direct git checkout of '%s' failed" % rev, p.output) else: # Check out the git rev equivalent to the svn rev p = self.git(['checkout', git_rev], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git checkout of '%s' failed") % rev, p.output) # Get rid of any uncontrolled files left behind p = self.git(['clean', '-dffx'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git clean failed"), p.output) def _gettags(self): self.checkrepo() for treeish in ['origin/', '']: d = os.path.join(self.local, '.git', 'svn', 'refs', 'remotes', treeish, 'tags') if os.path.isdir(d): return os.listdir(d) def getref(self): self.checkrepo() p = FDroidPopen(['git', 'svn', 'find-rev', 'HEAD'], cwd=self.local, output=False) if p.returncode != 0: return None return p.output.strip() class vcs_hg(vcs): def repotype(self): return 'hg' def clientversioncmd(self): return ['hg', '--version'] def gotorevisionx(self, rev): if not os.path.exists(self.local): p = FDroidPopen(['hg', 'clone', '--ssh', '/bin/false', '--', self.remote, self.local], output=False) if p.returncode != 0: self.clone_failed = True raise VCSException("Hg clone failed", p.output) else: p = FDroidPopen(['hg', 'status', '-uS'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Hg status failed", p.output) for line in p.output.splitlines(): if not line.startswith('? '): raise VCSException("Unexpected output from hg status -uS: " + line) FDroidPopen(['rm', '-rf', '--', line[2:]], cwd=self.local, output=False) if not self.refreshed: p = FDroidPopen(['hg', 'pull', '--ssh', '/bin/false'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Hg pull failed", p.output) self.refreshed = True rev = rev or 'default' if not rev: return p = FDroidPopen(['hg', 'update', '-C', '--', rev], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Hg checkout of '%s' failed" % rev, p.output) p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False) # Also delete untracked files, we have to enable purge extension for that: if "'purge' is provided by the following extension" in p.output: with open(os.path.join(self.local, '.hg', 'hgrc'), "a") as myfile: myfile.write("\n[extensions]\nhgext.purge=\n") p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("HG purge failed", p.output) elif p.returncode != 0: raise VCSException("HG purge failed", p.output) def _gettags(self): p = FDroidPopen(['hg', 'tags', '-q'], cwd=self.local, output=False) return p.output.splitlines()[1:] class vcs_bzr(vcs): def repotype(self): return 'bzr' def clientversioncmd(self): return ['bzr', '--version'] def bzr(self, args, envs=dict(), cwd=None, output=True): '''Prevent bzr from ever using SSH to avoid security vulns''' envs.update({ 'BZR_SSH': 'false', }) return FDroidPopen(['bzr', ] + args, envs=envs, cwd=cwd, output=output) def gotorevisionx(self, rev): if not os.path.exists(self.local): p = self.bzr(['branch', self.remote, self.local], output=False) if p.returncode != 0: self.clone_failed = True raise VCSException("Bzr branch failed", p.output) else: p = self.bzr(['clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Bzr revert failed", p.output) if not self.refreshed: p = self.bzr(['pull'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Bzr update failed", p.output) self.refreshed = True revargs = list(['-r', rev] if rev else []) p = self.bzr(['revert'] + revargs, cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Bzr revert of '%s' failed" % rev, p.output) def _gettags(self): p = self.bzr(['tags'], cwd=self.local, output=False) return [tag.split(' ')[0].strip() for tag in p.output.splitlines()] def unescape_string(string): if len(string) < 2: return string if string[0] == '"' and string[-1] == '"': return string[1:-1] return string.replace("\\'", "'") def retrieve_string(app_dir, string, xmlfiles=None): if not string.startswith('@string/'): return unescape_string(string) if xmlfiles is None: xmlfiles = [] for res_dir in [ os.path.join(app_dir, 'res'), os.path.join(app_dir, 'src', 'main', 'res'), ]: for root, dirs, files in os.walk(res_dir): if os.path.basename(root) == 'values': xmlfiles += [os.path.join(root, x) for x in files if x.endswith('.xml')] name = string[len('@string/'):] def element_content(element): if element.text is None: return "" s = XMLElementTree.tostring(element, encoding='utf-8', method='text') return s.decode('utf-8').strip() for path in xmlfiles: if not os.path.isfile(path): continue xml = parse_xml(path) element = xml.find('string[@name="' + name + '"]') if element is not None: content = element_content(element) return retrieve_string(app_dir, content, xmlfiles) return '' def retrieve_string_singleline(app_dir, string, xmlfiles=None): return retrieve_string(app_dir, string, xmlfiles).replace('\n', ' ').strip() def manifest_paths(app_dir, flavours): '''Return list of existing files that will be used to find the highest vercode''' possible_manifests = \ [os.path.join(app_dir, 'AndroidManifest.xml'), os.path.join(app_dir, 'src', 'main', 'AndroidManifest.xml'), os.path.join(app_dir, 'src', 'AndroidManifest.xml'), os.path.join(app_dir, 'build.gradle')] for flavour in flavours: if flavour == 'yes': continue possible_manifests.append( os.path.join(app_dir, 'src', flavour, 'AndroidManifest.xml')) return [path for path in possible_manifests if os.path.isfile(path)] def fetch_real_name(app_dir, flavours): '''Retrieve the package name. Returns the name, or None if not found.''' for path in manifest_paths(app_dir, flavours): if not has_extension(path, 'xml') or not os.path.isfile(path): continue logging.debug("fetch_real_name: Checking manifest at " + path) xml = parse_xml(path) app = xml.find('application') if app is None: continue if XMLNS_ANDROID + "label" not in app.attrib: continue label = app.attrib[XMLNS_ANDROID + "label"] result = retrieve_string_singleline(app_dir, label) if result: result = result.strip() return result return None def get_library_references(root_dir): libraries = [] proppath = os.path.join(root_dir, 'project.properties') if not os.path.isfile(proppath): return libraries with open(proppath, 'r', encoding='iso-8859-1') as f: for line in f: if not line.startswith('android.library.reference.'): continue path = line.split('=')[1].strip() relpath = os.path.join(root_dir, path) if not os.path.isdir(relpath): continue logging.debug("Found subproject at %s" % path) libraries.append(path) return libraries def ant_subprojects(root_dir): subprojects = get_library_references(root_dir) for subpath in subprojects: subrelpath = os.path.join(root_dir, subpath) for p in get_library_references(subrelpath): relp = os.path.normpath(os.path.join(subpath, p)) if relp not in subprojects: subprojects.insert(0, relp) return subprojects def remove_debuggable_flags(root_dir): # Remove forced debuggable flags logging.debug("Removing debuggable flags from %s" % root_dir) for root, dirs, files in os.walk(root_dir): if 'AndroidManifest.xml' in files and os.path.isfile(os.path.join(root, 'AndroidManifest.xml')): regsub_file(r'android:debuggable="[^"]*"', '', os.path.join(root, 'AndroidManifest.xml')) vcsearch_g = re.compile(r'''.*[Vv]ersionCode\s*=?\s*["']*([0-9]+)["']*''').search vnsearch_g = re.compile(r'''.*[Vv]ersionName\s*=?\s*(["'])((?:(?=(\\?))\3.)*?)\1.*''').search vnssearch_g = re.compile(r'''.*[Vv]ersionNameSuffix\s*=?\s*(["'])((?:(?=(\\?))\3.)*?)\1.*''').search psearch_g = re.compile(r'''.*(packageName|applicationId)\s*=*\s*["']([^"']+)["'].*''').search fsearch_g = re.compile(r'''.*(applicationIdSuffix)\s*=*\s*["']([^"']+)["'].*''').search def app_matches_packagename(app, package): if not package: return False appid = app.UpdateCheckName or app.id if appid is None or appid == "Ignore": return True return appid == package def parse_androidmanifests(paths, app): """ Extract some information from the AndroidManifest.xml at the given path. Returns (version, vercode, package), any or all of which might be None. All values returned are strings. """ ignoreversions = app.UpdateCheckIgnore ignoresearch = re.compile(ignoreversions).search if ignoreversions else None if not paths: return (None, None, None) max_version = None max_vercode = None max_package = None for path in paths: if not os.path.isfile(path): continue logging.debug(_("Parsing manifest at '{path}'").format(path=path)) version = None vercode = None package = None flavour = None temp_app_id = None temp_version_name = None if app.builds and 'gradle' in app.builds[-1] and app.builds[-1].gradle: flavour = app.builds[-1].gradle[-1] if has_extension(path, 'gradle'): with open(path, 'r') as f: inside_flavour_group = 0 inside_required_flavour = 0 for line in f: if gradle_comment.match(line): continue if "applicationId" in line and not temp_app_id: matches = psearch_g(line) if matches: temp_app_id = matches.group(2) if "versionName" in line and not temp_version_name: matches = vnsearch_g(line) if matches: temp_version_name = matches.group(2) if inside_flavour_group > 0: if inside_required_flavour > 0: matches = psearch_g(line) if matches: s = matches.group(2) if app_matches_packagename(app, s): package = s else: # If build.gradle contains applicationIdSuffix add it to the end of package name matches = fsearch_g(line) if matches and temp_app_id: suffix = matches.group(2) temp_app_id = temp_app_id + suffix if app_matches_packagename(app, temp_app_id): package = temp_app_id matches = vnsearch_g(line) if matches: version = matches.group(2) else: # If build.gradle contains applicationNameSuffix add it to the end of version name matches = vnssearch_g(line) if matches and temp_version_name: name_suffix = matches.group(2) version = temp_version_name + name_suffix matches = vcsearch_g(line) if matches: vercode = matches.group(1) if '{' in line: inside_required_flavour += 1 if '}' in line: inside_required_flavour -= 1 else: if flavour and (flavour in line): inside_required_flavour = 1 if '{' in line: inside_flavour_group += 1 if '}' in line: inside_flavour_group -= 1 else: if "productFlavors" in line: inside_flavour_group = 1 if not package: matches = psearch_g(line) if matches: s = matches.group(2) if app_matches_packagename(app, s): package = s if not version: matches = vnsearch_g(line) if matches: version = matches.group(2) if not vercode: matches = vcsearch_g(line) if matches: vercode = matches.group(1) else: try: xml = parse_xml(path) if "package" in xml.attrib: s = xml.attrib["package"] if app_matches_packagename(app, s): package = s if XMLNS_ANDROID + "versionName" in xml.attrib: version = xml.attrib[XMLNS_ANDROID + "versionName"] base_dir = os.path.dirname(path) version = retrieve_string_singleline(base_dir, version) if XMLNS_ANDROID + "versionCode" in xml.attrib: a = xml.attrib[XMLNS_ANDROID + "versionCode"] if string_is_integer(a): vercode = a except Exception: logging.warning(_("Problem with xml at '{path}'").format(path=path)) # Remember package name, may be defined separately from version+vercode if package is None: package = max_package logging.debug("..got package={0}, version={1}, vercode={2}" .format(package, version, vercode)) # Always grab the package name and version name in case they are not # together with the highest version code if max_package is None and package is not None: max_package = package if max_version is None and version is not None: max_version = version if vercode is not None \ and (max_vercode is None or vercode > max_vercode): if not ignoresearch or not ignoresearch(version): if version is not None: max_version = version if vercode is not None: max_vercode = vercode if package is not None: max_package = package else: max_version = "Ignore" if max_version is None: max_version = "Unknown" if max_package: msg = _("Invalid package name {0}").format(max_package) if not is_valid_package_name(max_package): raise FDroidException(msg) elif not is_strict_application_id(max_package): logging.warning(msg) return (max_version, max_vercode, max_package) def is_valid_package_name(name): """Check whether name is a valid fdroid package name APKs and manually defined package names must use a valid Java Package Name. Automatically generated package names for non-APK files use the SHA-256 sum. """ return VALID_APPLICATION_ID_REGEX.match(name) is not None \ or FDROID_PACKAGE_NAME_REGEX.match(name) is not None def is_strict_application_id(name): """Check whether name is a valid Android Application ID The Android ApplicationID is basically a Java Package Name, but with more restrictive naming rules: * It must have at least two segments (one or more dots). * Each segment must start with a letter. * All characters must be alphanumeric or an underscore [a-zA-Z0-9_]. https://developer.android.com/studio/build/application-id """ return STRICT_APPLICATION_ID_REGEX.match(name) is not None \ and '.' in name def getsrclib(spec, srclib_dir, subdir=None, basepath=False, raw=False, prepare=True, preponly=False, refresh=True, build=None): """Get the specified source library. Returns the path to it. Normally this is the path to be used when referencing it, which may be a subdirectory of the actual project. If you want the base directory of the project, pass 'basepath=True'. """ number = None subdir = None if raw: name = spec ref = None else: name, ref = spec.split('@') if ':' in name: number, name = name.split(':', 1) if '/' in name: name, subdir = name.split('/', 1) if name not in fdroidserver.metadata.srclibs: raise VCSException('srclib ' + name + ' not found.') srclib = fdroidserver.metadata.srclibs[name] sdir = os.path.join(srclib_dir, name) if not preponly: vcs = getvcs(srclib["Repo Type"], srclib["Repo"], sdir) vcs.srclib = (name, number, sdir) if ref: vcs.gotorevision(ref, refresh) if raw: return vcs libdir = None if subdir: libdir = os.path.join(sdir, subdir) elif srclib["Subdir"]: for subdir in srclib["Subdir"]: libdir_candidate = os.path.join(sdir, subdir) if os.path.exists(libdir_candidate): libdir = libdir_candidate break if libdir is None: libdir = sdir remove_signing_keys(sdir) remove_debuggable_flags(sdir) if prepare: if srclib["Prepare"]: cmd = replace_config_vars(srclib["Prepare"], build) p = FDroidPopen(['bash', '-x', '-c', '--', cmd], cwd=libdir) if p.returncode != 0: raise BuildException("Error running prepare command for srclib %s" % name, p.output) if basepath: libdir = sdir return (name, number, libdir) gradle_version_regex = re.compile(r"[^/]*'com\.android\.tools\.build:gradle:([^\.]+\.[^\.]+).*'.*") def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False, refresh=True): """ Prepare the source code for a particular build :param vcs: the appropriate vcs object for the application :param app: the application details from the metadata :param build: the build details from the metadata :param build_dir: the path to the build directory, usually 'build/app.id' :param srclib_dir: the path to the source libraries directory, usually 'build/srclib' :param extlib_dir: the path to the external libraries directory, usually 'build/extlib' Returns the (root, srclibpaths) where: :param root: is the root directory, which may be the same as 'build_dir' or may be a subdirectory of it. :param srclibpaths: is information on the srclibs being used """ # Optionally, the actual app source can be in a subdirectory if build.subdir: root_dir = os.path.join(build_dir, build.subdir) else: root_dir = build_dir # Get a working copy of the right revision logging.info("Getting source for revision " + build.commit) vcs.gotorevision(build.commit, refresh) # Initialise submodules if required if build.submodules: logging.info(_("Initialising submodules")) vcs.initsubmodules() # Check that a subdir (if we're using one) exists. This has to happen # after the checkout, since it might not exist elsewhere if not os.path.exists(root_dir): raise BuildException('Missing subdir ' + root_dir) # Run an init command if one is required if build.init: cmd = replace_config_vars(build.init, build) logging.info("Running 'init' commands in %s" % root_dir) p = FDroidPopen(['bash', '-x', '-c', '--', cmd], cwd=root_dir) if p.returncode != 0: raise BuildException("Error running init command for %s:%s" % (app.id, build.versionName), p.output) # Apply patches if any if build.patch: logging.info("Applying patches") for patch in build.patch: patch = patch.strip() logging.info("Applying " + patch) patch_path = os.path.join('metadata', app.id, patch) p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir) if p.returncode != 0: raise BuildException("Failed to apply patch %s" % patch_path) # Get required source libraries srclibpaths = [] if build.srclibs: logging.info("Collecting source libraries") for lib in build.srclibs: srclibpaths.append(getsrclib(lib, srclib_dir, build, preponly=onserver, refresh=refresh, build=build)) for name, number, libpath in srclibpaths: place_srclib(root_dir, int(number) if number else None, libpath) basesrclib = vcs.getsrclib() # If one was used for the main source, add that too. if basesrclib: srclibpaths.append(basesrclib) # Update the local.properties file localprops = [os.path.join(build_dir, 'local.properties')] if build.subdir: parts = build.subdir.split(os.sep) cur = build_dir for d in parts: cur = os.path.join(cur, d) localprops += [os.path.join(cur, 'local.properties')] for path in localprops: props = "" if os.path.isfile(path): logging.info("Updating local.properties file at %s" % path) with open(path, 'r', encoding='iso-8859-1') as f: props += f.read() props += '\n' else: logging.info("Creating local.properties file at %s" % path) # Fix old-fashioned 'sdk-location' by copying # from sdk.dir, if necessary if build.oldsdkloc: sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props, re.S | re.M).group(1) props += "sdk-location=%s\n" % sdkloc else: props += "sdk.dir=%s\n" % config['sdk_path'] props += "sdk-location=%s\n" % config['sdk_path'] ndk_path = build.ndk_path() # if for any reason the path isn't valid or the directory # doesn't exist, some versions of Gradle will error with a # cryptic message (even if the NDK is not even necessary). # https://gitlab.com/fdroid/fdroidserver/issues/171 if ndk_path and os.path.exists(ndk_path): # Add ndk location props += "ndk.dir=%s\n" % ndk_path props += "ndk-location=%s\n" % ndk_path # Add java.encoding if necessary if build.encoding: props += "java.encoding=%s\n" % build.encoding with open(path, 'w', encoding='iso-8859-1') as f: f.write(props) flavours = [] if build.build_method() == 'gradle': flavours = build.gradle if build.target: n = build.target.split('-')[1] regsub_file(r'compileSdkVersion[ =]+[0-9]+', r'compileSdkVersion %s' % n, os.path.join(root_dir, 'build.gradle')) # Remove forced debuggable flags remove_debuggable_flags(root_dir) # Insert version code and number into the manifest if necessary if build.forceversion: logging.info("Changing the version name") for path in manifest_paths(root_dir, flavours): if not os.path.isfile(path): continue if has_extension(path, 'xml'): regsub_file(r'android:versionName="[^"]*"', r'android:versionName="%s"' % build.versionName, path) elif has_extension(path, 'gradle'): regsub_file(r"""(\s*)versionName[\s'"=]+.*""", r"""\1versionName '%s'""" % build.versionName, path) if build.forcevercode: logging.info("Changing the version code") for path in manifest_paths(root_dir, flavours): if not os.path.isfile(path): continue if has_extension(path, 'xml'): regsub_file(r'android:versionCode="[^"]*"', r'android:versionCode="%s"' % build.versionCode, path) elif has_extension(path, 'gradle'): regsub_file(r'versionCode[ =]+[0-9]+', r'versionCode %s' % build.versionCode, path) # Delete unwanted files if build.rm: logging.info(_("Removing specified files")) for part in getpaths(build_dir, build.rm): dest = os.path.join(build_dir, part) logging.info("Removing {0}".format(part)) if os.path.lexists(dest): # rmtree can only handle directories that are not symlinks, so catch anything else if not os.path.isdir(dest) or os.path.islink(dest): os.remove(dest) else: shutil.rmtree(dest) else: logging.info("...but it didn't exist") remove_signing_keys(build_dir) # Add required external libraries if build.extlibs: logging.info("Collecting prebuilt libraries") libsdir = os.path.join(root_dir, 'libs') if not os.path.exists(libsdir): os.mkdir(libsdir) for lib in build.extlibs: lib = lib.strip() logging.info("...installing extlib {0}".format(lib)) libf = os.path.basename(lib) libsrc = os.path.join(extlib_dir, lib) if not os.path.exists(libsrc): raise BuildException("Missing extlib file {0}".format(libsrc)) shutil.copyfile(libsrc, os.path.join(libsdir, libf)) # Run a pre-build command if one is required if build.prebuild: logging.info("Running 'prebuild' commands in %s" % root_dir) cmd = replace_config_vars(build.prebuild, build) # Substitute source library paths into prebuild commands for name, number, libpath in srclibpaths: libpath = os.path.relpath(libpath, root_dir) cmd = cmd.replace('$$' + name + '$$', libpath) p = FDroidPopen(['bash', '-x', '-c', '--', cmd], cwd=root_dir) if p.returncode != 0: raise BuildException("Error running prebuild command for %s:%s" % (app.id, build.versionName), p.output) # Generate (or update) the ant build file, build.xml... if build.build_method() == 'ant' and build.androidupdate != ['no']: parms = ['android', 'update', 'lib-project'] lparms = ['android', 'update', 'project'] if build.target: parms += ['-t', build.target] lparms += ['-t', build.target] if build.androidupdate: update_dirs = build.androidupdate else: update_dirs = ant_subprojects(root_dir) + ['.'] for d in update_dirs: subdir = os.path.join(root_dir, d) if d == '.': logging.debug("Updating main project") cmd = parms + ['-p', d] else: logging.debug("Updating subproject %s" % d) cmd = lparms + ['-p', d] p = SdkToolsPopen(cmd, cwd=root_dir) # Check to see whether an error was returned without a proper exit # code (this is the case for the 'no target set or target invalid' # error) if p.returncode != 0 or p.output.startswith("Error: "): raise BuildException("Failed to update project at %s" % d, p.output) # Clean update dirs via ant if d != '.': logging.info("Cleaning subproject %s" % d) p = FDroidPopen(['ant', 'clean'], cwd=subdir) return (root_dir, srclibpaths) def getpaths_map(build_dir, globpaths): """Extend via globbing the paths from a field and return them as a map from original path to resulting paths""" paths = dict() for p in globpaths: p = p.strip() full_path = os.path.join(build_dir, p) full_path = os.path.normpath(full_path) paths[p] = [r[len(build_dir) + 1:] for r in glob.glob(full_path)] if not paths[p]: raise FDroidException("glob path '%s' did not match any files/dirs" % p) return paths def getpaths(build_dir, globpaths): """Extend via globbing the paths from a field and return them as a set""" paths_map = getpaths_map(build_dir, globpaths) paths = set() for k, v in paths_map.items(): for p in v: paths.add(p) return paths def natural_key(s): return [int(sp) if sp.isdigit() else sp for sp in re.split(r'(\d+)', s)] def check_system_clock(dt_obj, path): """Check if system clock is updated based on provided date If an APK has files newer than the system time, suggest updating the system clock. This is useful for offline systems, used for signing, which do not have another source of clock sync info. It has to be more than 24 hours newer because ZIP/APK files do not store timezone info """ checkdt = dt_obj - timedelta(1) if datetime.today() < checkdt: logging.warning(_('System clock is older than date in {path}!').format(path=path) + '\n' + _('Set clock to that time using:') + '\n' + 'sudo date -s "' + str(dt_obj) + '"') class KnownApks: """permanent store of existing APKs with the date they were added This is currently the only way to permanently store the "updated" date of APKs. """ def __init__(self): '''Load filename/date info about previously seen APKs Since the appid and date strings both will never have spaces, this is parsed as a list from the end to allow the filename to have any combo of spaces. ''' self.path = os.path.join('stats', 'known_apks.txt') self.apks = {} if os.path.isfile(self.path): with open(self.path, 'r') as f: for line in f: t = line.rstrip().split(' ') if len(t) == 2: self.apks[t[0]] = (t[1], None) else: appid = t[-2] date = datetime.strptime(t[-1], '%Y-%m-%d') filename = line[0:line.rfind(appid) - 1] self.apks[filename] = (appid, date) check_system_clock(date, self.path) self.changed = False def writeifchanged(self): if not self.changed: return if not os.path.exists('stats'): os.mkdir('stats') lst = [] for apk, app in self.apks.items(): appid, added = app line = apk + ' ' + appid if added: line += ' ' + added.strftime('%Y-%m-%d') lst.append(line) with open(self.path, 'w') as f: for line in sorted(lst, key=natural_key): f.write(line + '\n') def recordapk(self, apkName, app, default_date=None): ''' Record an apk (if it's new, otherwise does nothing) Returns the date it was added as a datetime instance ''' if apkName not in self.apks: if default_date is None: default_date = datetime.utcnow() self.apks[apkName] = (app, default_date) self.changed = True _ignored, added = self.apks[apkName] return added def getapp(self, apkname): """Look up information - given the 'apkname', returns (app id, date added/None). Or returns None for an unknown apk. """ if apkname in self.apks: return self.apks[apkname] return None def getlatest(self, num): """Get the most recent 'num' apps added to the repo, as a list of package ids with the most recent first""" apps = {} for apk, app in self.apks.items(): appid, added = app if added: if appid in apps: if apps[appid] > added: apps[appid] = added else: apps[appid] = added sortedapps = sorted(apps.items(), key=operator.itemgetter(1))[-num:] lst = [app for app, _ignored in sortedapps] lst.reverse() return lst def get_file_extension(filename): """get the normalized file extension, can be blank string but never None""" if isinstance(filename, bytes): filename = filename.decode('utf-8') return os.path.splitext(filename)[1].lower()[1:] def use_androguard(): """Report if androguard is available, and config its debug logging""" try: import androguard if use_androguard.show_path: logging.debug(_('Using androguard from "{path}"').format(path=androguard.__file__)) use_androguard.show_path = False if options and options.verbose: logging.getLogger("androguard.axml").setLevel(logging.INFO) return True except ImportError: return False use_androguard.show_path = True def _get_androguard_APK(apkfile): try: from androguard.core.bytecodes.apk import APK except ImportError: raise FDroidException("androguard library is not installed and aapt not present") return APK(apkfile) def ensure_final_value(packageName, arsc, value): """Ensure incoming value is always the value, not the resid androguard will sometimes return the Android "resId" aka Resource ID instead of the actual value. This checks whether the value is actually a resId, then performs the Android Resource lookup as needed. """ if value: returnValue = value if value[0] == '@': try: # can be a literal value or a resId res_id = int('0x' + value[1:], 16) res_id = arsc.get_id(packageName, res_id)[1] returnValue = arsc.get_string(packageName, res_id)[1] except (ValueError, TypeError): pass return returnValue return '' def is_apk_and_debuggable_aapt(apkfile): p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'], output=False) if p.returncode != 0: raise FDroidException(_("Failed to get APK manifest information")) for line in p.output.splitlines(): if 'android:debuggable' in line and not line.endswith('0x0'): return True return False def is_apk_and_debuggable_androguard(apkfile): """Parse only from the APK""" from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG with ZipFile(apkfile) as apk: with apk.open('AndroidManifest.xml') as manifest: axml = AXMLParser(manifest.read()) while axml.is_valid(): _type = next(axml) if _type == START_TAG and axml.getName() == 'application': for i in range(0, axml.getAttributeCount()): name = axml.getAttributeName(i) if name == 'debuggable': _type = axml.getAttributeValueType(i) _data = axml.getAttributeValueData(i) value = format_value(_type, _data, lambda _: axml.getAttributeValue(i)) if value == 'true': return True else: return False break return False def is_apk_and_debuggable(apkfile): """Returns True if the given file is an APK and is debuggable :param apkfile: full path to the apk to check""" if get_file_extension(apkfile) != 'apk': return False if use_androguard(): return is_apk_and_debuggable_androguard(apkfile) else: return is_apk_and_debuggable_aapt(apkfile) def get_apk_id(apkfile): """Extract identification information from APK. Androguard is preferred since it is more reliable and a lot faster. Occasionally, when androguard can't get the info from the APK, aapt still can. So aapt is also used as the final fallback method. :param apkfile: path to an APK file. :returns: triplet (appid, version code, version name) """ if use_androguard(): try: return get_apk_id_androguard(apkfile) except zipfile.BadZipFile as e: logging.error(apkfile + ': ' + str(e)) if 'aapt' in config: return get_apk_id_aapt(apkfile) else: return get_apk_id_aapt(apkfile) def get_apk_id_androguard(apkfile): """Read (appid, versionCode, versionName) from an APK This first tries to do quick binary XML parsing to just get the values that are needed. It will fallback to full androguard parsing, which is slow, if it can't find the versionName value or versionName is set to a Android String Resource (e.g. an integer hex value that starts with @). """ if not os.path.exists(apkfile): raise FDroidException(_("Reading packageName/versionCode/versionName failed, APK invalid: '{apkfilename}'") .format(apkfilename=apkfile)) from androguard.core.bytecodes.axml import AXMLParser, format_value, START_TAG, END_TAG, TEXT, END_DOCUMENT appid = None versionCode = None versionName = None with zipfile.ZipFile(apkfile) as apk: with apk.open('AndroidManifest.xml') as manifest: axml = AXMLParser(manifest.read()) count = 0 while axml.is_valid(): _type = next(axml) count += 1 if _type == START_TAG: for i in range(0, axml.getAttributeCount()): name = axml.getAttributeName(i) _type = axml.getAttributeValueType(i) _data = axml.getAttributeValueData(i) value = format_value(_type, _data, lambda _: axml.getAttributeValue(i)) if appid is None and name == 'package': appid = value elif versionCode is None and name == 'versionCode': if value.startswith('0x'): versionCode = str(int(value, 16)) else: versionCode = value elif versionName is None and name == 'versionName': versionName = value if axml.getName() == 'manifest': break elif _type == END_TAG or _type == TEXT or _type == END_DOCUMENT: raise RuntimeError('{path}: must be the first element in AndroidManifest.xml' .format(path=apkfile)) if not versionName or versionName[0] == '@': a = _get_androguard_APK(apkfile) versionName = ensure_final_value(a.package, a.get_android_resources(), a.get_androidversion_name()) if not versionName: versionName = '' # versionName is expected to always be a str return appid, versionCode, versionName.strip('\0') def get_apk_id_aapt(apkfile): p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False) m = APK_ID_TRIPLET_REGEX.match(p.output[0:p.output.index('\n')]) if m: return m.group(1), m.group(2), m.group(3) raise FDroidException(_("Reading packageName/versionCode/versionName failed, APK invalid: '{apkfilename}'") .format(apkfilename=apkfile)) def get_native_code(apkfile): """aapt checks if there are architecture folders under the lib/ folder so we are simulating the same behaviour""" arch_re = re.compile("^lib/(.*)/.*$") archset = set() with ZipFile(apkfile) as apk: for filename in apk.namelist(): m = arch_re.match(filename) if m: archset.add(m.group(1)) return sorted(list(archset)) def get_minSdkVersion_aapt(apkfile): """Extract the minimum supported Android SDK from an APK using aapt :param apkfile: path to an APK file. :returns: the integer representing the SDK version """ r = re.compile(r"^sdkVersion:'([0-9]+)'") p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False) for line in p.output.splitlines(): m = r.match(line) if m: return int(m.group(1)) raise FDroidException(_('Reading minSdkVersion failed: "{apkfilename}"') .format(apkfilename=apkfile)) class PopenResult: def __init__(self): self.returncode = None self.output = None def SdkToolsPopen(commands, cwd=None, output=True): cmd = commands[0] if cmd not in config: config[cmd] = find_sdk_tools_cmd(commands[0]) abscmd = config[cmd] if abscmd is None: raise FDroidException(_("Could not find '{command}' on your system").format(command=cmd)) if cmd == 'aapt': test_aapt_version(config['aapt']) return FDroidPopen([abscmd] + commands[1:], cwd=cwd, output=output) def FDroidPopenBytes(commands, cwd=None, envs=None, output=True, stderr_to_stdout=True): """ Run a command and capture the possibly huge output as bytes. :param commands: command and argument list like in subprocess.Popen :param cwd: optionally specifies a working directory :param envs: a optional dictionary of environment variables and their values :returns: A PopenResult. """ global env if env is None: set_FDroidPopen_env() process_env = env.copy() if envs is not None and len(envs) > 0: process_env.update(envs) if cwd: cwd = os.path.normpath(cwd) logging.debug("Directory: %s" % cwd) logging.debug("> %s" % ' '.join(commands)) stderr_param = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE result = PopenResult() p = None try: p = subprocess.Popen(commands, cwd=cwd, shell=False, env=process_env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=stderr_param) except OSError as e: raise BuildException("OSError while trying to execute " + ' '.join(commands) + ': ' + str(e)) # TODO are these AsynchronousFileReader threads always exiting? if not stderr_to_stdout and options.verbose: stderr_queue = Queue() stderr_reader = AsynchronousFileReader(p.stderr, stderr_queue) while not stderr_reader.eof(): while not stderr_queue.empty(): line = stderr_queue.get() sys.stderr.buffer.write(line) sys.stderr.flush() time.sleep(0.1) stdout_queue = Queue() stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue) buf = io.BytesIO() # Check the queue for output (until there is no more to get) while not stdout_reader.eof(): while not stdout_queue.empty(): line = stdout_queue.get() if output and options.verbose: # Output directly to console sys.stderr.buffer.write(line) sys.stderr.flush() buf.write(line) time.sleep(0.1) result.returncode = p.wait() result.output = buf.getvalue() buf.close() # make sure all filestreams of the subprocess are closed for streamvar in ['stdin', 'stdout', 'stderr']: if hasattr(p, streamvar): stream = getattr(p, streamvar) if stream: stream.close() return result def FDroidPopen(commands, cwd=None, envs=None, output=True, stderr_to_stdout=True): """ Run a command and capture the possibly huge output as a str. :param commands: command and argument list like in subprocess.Popen :param cwd: optionally specifies a working directory :param envs: a optional dictionary of environment variables and their values :returns: A PopenResult. """ result = FDroidPopenBytes(commands, cwd, envs, output, stderr_to_stdout) result.output = result.output.decode('utf-8', 'ignore') return result gradle_comment = re.compile(r'[ ]*//') gradle_signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$') gradle_line_matches = [ re.compile(r'^[\t ]*signingConfig [^ ]*$'), re.compile(r'.*android\.signingConfigs\.[^{]*$'), re.compile(r'.*\.readLine\(.*'), ] def remove_signing_keys(build_dir): for root, dirs, files in os.walk(build_dir): if 'build.gradle' in files: path = os.path.join(root, 'build.gradle') with open(path, "r") as o: lines = o.readlines() changed = False opened = 0 i = 0 with open(path, "w") as o: while i < len(lines): line = lines[i] i += 1 while line.endswith('\\\n'): line = line.rstrip('\\\n') + lines[i] i += 1 if gradle_comment.match(line): o.write(line) continue if opened > 0: opened += line.count('{') opened -= line.count('}') continue if gradle_signing_configs.match(line): changed = True opened += 1 continue if any(s.match(line) for s in gradle_line_matches): changed = True continue if opened == 0: o.write(line) if changed: logging.info("Cleaned build.gradle of keysigning configs at %s" % path) for propfile in [ 'project.properties', 'build.properties', 'default.properties', 'ant.properties', ]: if propfile in files: path = os.path.join(root, propfile) with open(path, "r", encoding='iso-8859-1') as o: lines = o.readlines() changed = False with open(path, "w", encoding='iso-8859-1') as o: for line in lines: if any(line.startswith(s) for s in ('key.store', 'key.alias')): changed = True continue o.write(line) if changed: logging.info("Cleaned %s of keysigning configs at %s" % (propfile, path)) def set_FDroidPopen_env(build=None): ''' set up the environment variables for the build environment There is only a weak standard, the variables used by gradle, so also set up the most commonly used environment variables for SDK and NDK. Also, if there is no locale set, this will set the locale (e.g. LANG) to en_US.UTF-8. ''' global env, orig_path if env is None: env = os.environ orig_path = env['PATH'] for n in ['ANDROID_HOME', 'ANDROID_SDK']: env[n] = config['sdk_path'] for k, v in config['java_paths'].items(): env['JAVA%s_HOME' % k] = v missinglocale = True for k, v in env.items(): if k == 'LANG' and v != 'C': missinglocale = False elif k == 'LC_ALL': missinglocale = False if missinglocale: env['LANG'] = 'en_US.UTF-8' if build is not None: path = build.ndk_path() paths = orig_path.split(os.pathsep) if path not in paths: paths = [path] + paths env['PATH'] = os.pathsep.join(paths) for n in ['ANDROID_NDK', 'NDK', 'ANDROID_NDK_HOME']: env[n] = build.ndk_path() def replace_build_vars(cmd, build): cmd = cmd.replace('$$COMMIT$$', build.commit) cmd = cmd.replace('$$VERSION$$', build.versionName) cmd = cmd.replace('$$VERCODE$$', build.versionCode) return cmd def replace_config_vars(cmd, build): cmd = cmd.replace('$$SDK$$', config['sdk_path']) cmd = cmd.replace('$$NDK$$', build.ndk_path()) cmd = cmd.replace('$$MVN3$$', config['mvn3']) if build is not None: cmd = replace_build_vars(cmd, build) return cmd def place_srclib(root_dir, number, libpath): if not number: return relpath = os.path.relpath(libpath, root_dir) proppath = os.path.join(root_dir, 'project.properties') lines = [] if os.path.isfile(proppath): with open(proppath, "r", encoding='iso-8859-1') as o: lines = o.readlines() with open(proppath, "w", encoding='iso-8859-1') as o: placed = False for line in lines: if line.startswith('android.library.reference.%d=' % number): o.write('android.library.reference.%d=%s\n' % (number, relpath)) placed = True else: o.write(line) if not placed: o.write('android.library.reference.%d=%s\n' % (number, relpath)) APK_SIGNATURE_FILES = re.compile(r'META-INF/[0-9A-Za-z_\-]+\.(SF|RSA|DSA|EC)') def signer_fingerprint_short(cert_encoded): """Obtain shortened sha256 signing-key fingerprint for pkcs7 DER certficate. Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint for a given pkcs7 signature. :param cert_encoded: Contents of an APK signing certificate. :returns: shortened signing-key fingerprint. """ return signer_fingerprint(cert_encoded)[:7] def signer_fingerprint(cert_encoded): """Obtain sha256 signing-key fingerprint for pkcs7 DER certificate. Extracts hexadecimal sha256 signing-key fingerprint string for a given pkcs7 signature. :param: Contents of an APK signature. :returns: shortened signature fingerprint. """ return hashlib.sha256(cert_encoded).hexdigest() def get_first_signer_certificate(apkpath): """Get the first signing certificate from the APK, DER-encoded""" certs = None cert_encoded = None with zipfile.ZipFile(apkpath, 'r') as apk: cert_files = [n for n in apk.namelist() if SIGNATURE_BLOCK_FILE_REGEX.match(n)] if len(cert_files) > 1: logging.error(_("Found multiple JAR Signature Block Files in {path}").format(path=apkpath)) return None elif len(cert_files) == 1: cert_encoded = get_certificate(apk.read(cert_files[0])) if not cert_encoded and use_androguard(): apkobject = _get_androguard_APK(apkpath) certs = apkobject.get_certificates_der_v2() if len(certs) > 0: logging.info(_('Using APK Signature v2')) cert_encoded = certs[0] if not cert_encoded: certs = apkobject.get_certificates_der_v3() if len(certs) > 0: logging.info(_('Using APK Signature v3')) cert_encoded = certs[0] if not cert_encoded: logging.error(_("No signing certificates found in {path}").format(path=apkpath)) return None return cert_encoded def apk_signer_fingerprint(apk_path): """Obtain sha256 signing-key fingerprint for APK. Extracts hexadecimal sha256 signing-key fingerprint string for a given APK. :param apk_path: path to APK :returns: signature fingerprint """ cert_encoded = get_first_signer_certificate(apk_path) if not cert_encoded: return None return signer_fingerprint(cert_encoded) def apk_signer_fingerprint_short(apk_path): """Obtain shortened sha256 signing-key fingerprint for APK. Extracts the first 7 hexadecimal digits of sha256 signing-key fingerprint for a given pkcs7 APK. :param apk_path: path to APK :returns: shortened signing-key fingerprint """ return apk_signer_fingerprint(apk_path)[:7] def metadata_get_sigdir(appid, vercode=None): """Get signature directory for app""" if vercode: return os.path.join('metadata', appid, 'signatures', vercode) else: return os.path.join('metadata', appid, 'signatures') def metadata_find_developer_signature(appid, vercode=None): """Tires to find the developer signature for given appid. This picks the first signature file found in metadata an returns its signature. :returns: sha256 signing key fingerprint of the developer signing key. None in case no signature can not be found.""" # fetch list of dirs for all versions of signatures appversigdirs = [] if vercode: appversigdirs.append(metadata_get_sigdir(appid, vercode)) else: appsigdir = metadata_get_sigdir(appid) if os.path.isdir(appsigdir): numre = re.compile('[0-9]+') for ver in os.listdir(appsigdir): if numre.match(ver): appversigdir = os.path.join(appsigdir, ver) appversigdirs.append(appversigdir) for sigdir in appversigdirs: sigs = glob.glob(os.path.join(sigdir, '*.DSA')) + \ glob.glob(os.path.join(sigdir, '*.EC')) + \ glob.glob(os.path.join(sigdir, '*.RSA')) if len(sigs) > 1: raise FDroidException('ambiguous signatures, please make sure there is only one signature in \'{}\'. (The signature has to be the App maintainers signature for version of the APK.)'.format(sigdir)) for sig in sigs: with open(sig, 'rb') as f: return signer_fingerprint(get_certificate(f.read())) return None def metadata_find_signing_files(appid, vercode): """Gets a list of singed manifests and signatures. :param appid: app id string :param vercode: app version code :returns: a list of triplets for each signing key with following paths: (signature_file, singed_file, manifest_file) """ ret = [] sigdir = metadata_get_sigdir(appid, vercode) sigs = glob.glob(os.path.join(sigdir, '*.DSA')) + \ glob.glob(os.path.join(sigdir, '*.EC')) + \ glob.glob(os.path.join(sigdir, '*.RSA')) extre = re.compile(r'(\.DSA|\.EC|\.RSA)$') for sig in sigs: sf = extre.sub('.SF', sig) if os.path.isfile(sf): mf = os.path.join(sigdir, 'MANIFEST.MF') if os.path.isfile(mf): ret.append((sig, sf, mf)) return ret def metadata_find_developer_signing_files(appid, vercode): """Get developer signature files for specified app from metadata. :returns: A triplet of paths for signing files from metadata: (signature_file, singed_file, manifest_file) """ allsigningfiles = metadata_find_signing_files(appid, vercode) if allsigningfiles and len(allsigningfiles) == 1: return allsigningfiles[0] else: return None def apk_strip_signatures(signed_apk, strip_manifest=False): """Removes signatures from APK. :param signed_apk: path to apk file. :param strip_manifest: when set to True also the manifest file will be removed from the APK. """ with tempfile.TemporaryDirectory() as tmpdir: tmp_apk = os.path.join(tmpdir, 'tmp.apk') shutil.move(signed_apk, tmp_apk) with ZipFile(tmp_apk, 'r') as in_apk: with ZipFile(signed_apk, 'w') as out_apk: for info in in_apk.infolist(): if not APK_SIGNATURE_FILES.match(info.filename): if strip_manifest: if info.filename != 'META-INF/MANIFEST.MF': buf = in_apk.read(info.filename) out_apk.writestr(info, buf) else: buf = in_apk.read(info.filename) out_apk.writestr(info, buf) def _zipalign(unsigned_apk, aligned_apk): """run 'zipalign' using standard flags used by Gradle Android Plugin -p was added in build-tools-23.0.0 https://developer.android.com/studio/publish/app-signing#sign-manually """ p = SdkToolsPopen(['zipalign', '-v', '-p', '4', unsigned_apk, aligned_apk]) if p.returncode != 0: raise BuildException("Failed to align application") def apk_implant_signatures(apkpath, signaturefile, signedfile, manifest): """Implats a signature from metadata into an APK. Note: this changes there supplied APK in place. So copy it if you need the original to be preserved. :param apkpath: location of the apk """ # get list of available signature files in metadata with tempfile.TemporaryDirectory() as tmpdir: apkwithnewsig = os.path.join(tmpdir, 'newsig.apk') with ZipFile(apkpath, 'r') as in_apk: with ZipFile(apkwithnewsig, 'w') as out_apk: for sig_file in [signaturefile, signedfile, manifest]: with open(sig_file, 'rb') as fp: buf = fp.read() info = zipfile.ZipInfo('META-INF/' + os.path.basename(sig_file)) info.compress_type = zipfile.ZIP_DEFLATED info.create_system = 0 # "Windows" aka "FAT", what Android SDK uses out_apk.writestr(info, buf) for info in in_apk.infolist(): if not APK_SIGNATURE_FILES.match(info.filename): if info.filename != 'META-INF/MANIFEST.MF': buf = in_apk.read(info.filename) out_apk.writestr(info, buf) os.remove(apkpath) _zipalign(apkwithnewsig, apkpath) def apk_extract_signatures(apkpath, outdir, manifest=True): """Extracts a signature files from APK and puts them into target directory. :param apkpath: location of the apk :param outdir: folder where the extracted signature files will be stored :param manifest: (optionally) disable extracting manifest file """ with ZipFile(apkpath, 'r') as in_apk: for f in in_apk.infolist(): if APK_SIGNATURE_FILES.match(f.filename) or \ (manifest and f.filename == 'META-INF/MANIFEST.MF'): newpath = os.path.join(outdir, os.path.basename(f.filename)) with open(newpath, 'wb') as out_file: out_file.write(in_apk.read(f.filename)) def sign_apk(unsigned_path, signed_path, keyalias): """Sign and zipalign an unsigned APK, then save to a new file, deleting the unsigned android-18 (4.3) finally added support for reasonable hash algorithms, like SHA-256, before then, the only options were MD5 and SHA1 :-/ This aims to use SHA-256 when the APK does not target older Android versions, and is therefore safe to do so. https://issuetracker.google.com/issues/36956587 https://android-review.googlesource.com/c/platform/libcore/+/44491 """ if get_minSdkVersion_aapt(unsigned_path) < 18: signature_algorithm = ['-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1'] else: signature_algorithm = ['-sigalg', 'SHA256withRSA', '-digestalg', 'SHA-256'] p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'], '-storepass:env', 'FDROID_KEY_STORE_PASS', '-keypass:env', 'FDROID_KEY_PASS'] + signature_algorithm + [unsigned_path, keyalias], envs={ 'FDROID_KEY_STORE_PASS': config['keystorepass'], 'FDROID_KEY_PASS': config['keypass'], }) if p.returncode != 0: raise BuildException(_("Failed to sign application"), p.output) _zipalign(unsigned_path, signed_path) os.remove(unsigned_path) def verify_apks(signed_apk, unsigned_apk, tmp_dir): """Verify that two apks are the same One of the inputs is signed, the other is unsigned. The signature metadata is transferred from the signed to the unsigned apk, and then jarsigner is used to verify that the signature from the signed apk is also varlid for the unsigned one. If the APK given as unsigned actually does have a signature, it will be stripped out and ignored. :param signed_apk: Path to a signed apk file :param unsigned_apk: Path to an unsigned apk file expected to match it :param tmp_dir: Path to directory for temporary files :returns: None if the verification is successful, otherwise a string describing what went wrong. """ if not os.path.isfile(signed_apk): return 'can not verify: file does not exists: {}'.format(signed_apk) if not os.path.isfile(unsigned_apk): return 'can not verify: file does not exists: {}'.format(unsigned_apk) with ZipFile(signed_apk, 'r') as signed: meta_inf_files = ['META-INF/MANIFEST.MF'] for f in signed.namelist(): if APK_SIGNATURE_FILES.match(f): meta_inf_files.append(f) if len(meta_inf_files) < 3: return "Signature files missing from {0}".format(signed_apk) tmp_apk = os.path.join(tmp_dir, 'sigcp_' + os.path.basename(unsigned_apk)) with ZipFile(unsigned_apk, 'r') as unsigned: # only read the signature from the signed APK, everything else from unsigned with ZipFile(tmp_apk, 'w') as tmp: for filename in meta_inf_files: tmp.writestr(signed.getinfo(filename), signed.read(filename)) for info in unsigned.infolist(): if info.filename in meta_inf_files: logging.warning('Ignoring %s from %s', info.filename, unsigned_apk) continue if info.filename in tmp.namelist(): return "duplicate filename found: " + info.filename tmp.writestr(info, unsigned.read(info.filename)) verified = verify_apk_signature(tmp_apk) if not verified: logging.info("...NOT verified - {0}".format(tmp_apk)) return compare_apks(signed_apk, tmp_apk, tmp_dir, os.path.dirname(unsigned_apk)) logging.info("...successfully verified") return None def verify_jar_signature(jar): """Verifies the signature of a given JAR file. jarsigner is very shitty: unsigned JARs pass as "verified"! So this has to turn on -strict then check for result 4, since this does not expect the signature to be from a CA-signed certificate. :raises: VerificationException() if the JAR's signature could not be verified """ error = _('JAR signature failed to verify: {path}').format(path=jar) try: output = subprocess.check_output([config['jarsigner'], '-strict', '-verify', jar], stderr=subprocess.STDOUT) raise VerificationException(error + '\n' + output.decode('utf-8')) except subprocess.CalledProcessError as e: if e.returncode == 4: logging.debug(_('JAR signature verified: {path}').format(path=jar)) else: raise VerificationException(error + '\n' + e.output.decode('utf-8')) def verify_apk_signature(apk, min_sdk_version=None): """verify the signature on an APK Try to use apksigner whenever possible since jarsigner is very shitty: unsigned APKs pass as "verified"! Warning, this does not work on JARs with apksigner >= 0.7 (build-tools 26.0.1) :returns: boolean whether the APK was verified """ if set_command_in_config('apksigner'): args = [config['apksigner'], 'verify'] if min_sdk_version: args += ['--min-sdk-version=' + min_sdk_version] if options.verbose: args += ['--verbose'] try: output = subprocess.check_output(args + [apk]) if options.verbose: logging.debug(apk + ': ' + output.decode('utf-8')) return True except subprocess.CalledProcessError as e: logging.error('\n' + apk + ': ' + e.output.decode('utf-8')) else: if not config.get('jarsigner_warning_displayed'): config['jarsigner_warning_displayed'] = True logging.warning(_("Using Java's jarsigner, not recommended for verifying APKs! Use apksigner")) try: verify_jar_signature(apk) return True except Exception as e: logging.error(e) return False def verify_old_apk_signature(apk): """verify the signature on an archived APK, supporting deprecated algorithms F-Droid aims to keep every single binary that it ever published. Therefore, it needs to be able to verify APK signatures that include deprecated/removed algorithms. For example, jarsigner treats an MD5 signature as unsigned. jarsigner passes unsigned APKs as "verified"! So this has to turn on -strict then check for result 4. Just to be safe, this never reuses the file, and locks down the file permissions while in use. That should prevent a bad actor from changing the settings during operation. :returns: boolean whether the APK was verified """ _java_security = os.path.join(os.getcwd(), '.java.security') if os.path.exists(_java_security): os.remove(_java_security) with open(_java_security, 'w') as fp: fp.write('jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024') os.chmod(_java_security, 0o400) try: cmd = [ config['jarsigner'], '-J-Djava.security.properties=' + _java_security, '-strict', '-verify', apk ] output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: if e.returncode != 4: output = e.output else: logging.debug(_('JAR signature verified: {path}').format(path=apk)) return True finally: if os.path.exists(_java_security): os.chmod(_java_security, 0o600) os.remove(_java_security) logging.error(_('Old APK signature failed to verify: {path}').format(path=apk) + '\n' + output.decode('utf-8')) return False apk_badchars = re.compile('''[/ :;'"]''') def compare_apks(apk1, apk2, tmp_dir, log_dir=None): """Compare two apks Returns None if the apk content is the same (apart from the signing key), otherwise a string describing what's different, or what went wrong when trying to do the comparison. """ if not log_dir: log_dir = tmp_dir absapk1 = os.path.abspath(apk1) absapk2 = os.path.abspath(apk2) if set_command_in_config('diffoscope'): logfilename = os.path.join(log_dir, os.path.basename(absapk1)) htmlfile = logfilename + '.diffoscope.html' textfile = logfilename + '.diffoscope.txt' if subprocess.call([config['diffoscope'], '--max-report-size', '12345678', '--max-diff-block-lines', '128', '--html', htmlfile, '--text', textfile, absapk1, absapk2]) != 0: return("Failed to run diffoscope " + apk1) apk1dir = os.path.join(tmp_dir, apk_badchars.sub('_', apk1[0:-4])) # trim .apk apk2dir = os.path.join(tmp_dir, apk_badchars.sub('_', apk2[0:-4])) # trim .apk for d in [apk1dir, apk2dir]: if os.path.exists(d): shutil.rmtree(d) os.mkdir(d) os.mkdir(os.path.join(d, 'content')) # extract APK contents for comparision with ZipFile(absapk1, 'r') as f: f.extractall(path=os.path.join(apk1dir, 'content')) with ZipFile(absapk2, 'r') as f: f.extractall(path=os.path.join(apk2dir, 'content')) if set_command_in_config('apktool'): if subprocess.call([config['apktool'], 'd', absapk1, '--output', 'apktool'], cwd=apk1dir) != 0: return("Failed to run apktool " + apk1) if subprocess.call([config['apktool'], 'd', absapk2, '--output', 'apktool'], cwd=apk2dir) != 0: return("Failed to run apktool " + apk2) p = FDroidPopen(['diff', '-r', apk1dir, apk2dir], output=False) lines = p.output.splitlines() if len(lines) != 1 or 'META-INF' not in lines[0]: if set_command_in_config('meld'): p = FDroidPopen([config['meld'], apk1dir, apk2dir], output=False) return("Unexpected diff output:\n" + p.output) # since everything verifies, delete the comparison to keep cruft down shutil.rmtree(apk1dir) shutil.rmtree(apk2dir) # If we get here, it seems like they're the same! return None def set_command_in_config(command): '''Try to find specified command in the path, if it hasn't been manually set in config.py. If found, it is added to the config dict. The return value says whether the command is available. ''' if command in config: return True else: tmp = find_command(command) if tmp is not None: config[command] = tmp return True return False def find_command(command): '''find the full path of a command, or None if it can't be found in the PATH''' def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) fpath, fname = os.path.split(command) if fpath: if is_exe(command): return command else: for path in os.environ["PATH"].split(os.pathsep): path = path.strip('"') exe_file = os.path.join(path, command) if is_exe(exe_file): return exe_file return None def genpassword(): '''generate a random password for when generating keys''' h = hashlib.sha256() h.update(os.urandom(16)) # salt h.update(socket.getfqdn().encode('utf-8')) passwd = base64.b64encode(h.digest()).strip() return passwd.decode('utf-8') def genkeystore(localconfig): """ Generate a new key with password provided in :param localconfig and add it to new keystore :return: hexed public key, public key fingerprint """ logging.info('Generating a new key in "' + localconfig['keystore'] + '"...') keystoredir = os.path.dirname(localconfig['keystore']) if keystoredir is None or keystoredir == '': keystoredir = os.path.join(os.getcwd(), keystoredir) if not os.path.exists(keystoredir): os.makedirs(keystoredir, mode=0o700) env_vars = {'LC_ALL': 'C.UTF-8', 'FDROID_KEY_STORE_PASS': localconfig['keystorepass'], 'FDROID_KEY_PASS': localconfig['keypass']} p = FDroidPopen([config['keytool'], '-genkey', '-keystore', localconfig['keystore'], '-alias', localconfig['repo_keyalias'], '-keyalg', 'RSA', '-keysize', '4096', '-sigalg', 'SHA256withRSA', '-validity', '10000', '-storepass:env', 'FDROID_KEY_STORE_PASS', '-keypass:env', 'FDROID_KEY_PASS', '-dname', localconfig['keydname'], '-J-Duser.language=en'], envs=env_vars) if p.returncode != 0: raise BuildException("Failed to generate key", p.output) os.chmod(localconfig['keystore'], 0o0600) if not options.quiet: # now show the lovely key that was just generated p = FDroidPopen([config['keytool'], '-list', '-v', '-keystore', localconfig['keystore'], '-alias', localconfig['repo_keyalias'], '-storepass:env', 'FDROID_KEY_STORE_PASS', '-J-Duser.language=en'], envs=env_vars) logging.info(p.output.strip() + '\n\n') # get the public key p = FDroidPopenBytes([config['keytool'], '-exportcert', '-keystore', localconfig['keystore'], '-alias', localconfig['repo_keyalias'], '-storepass:env', 'FDROID_KEY_STORE_PASS'] + config['smartcardoptions'], envs=env_vars, output=False, stderr_to_stdout=False) if p.returncode != 0 or len(p.output) < 20: raise BuildException("Failed to get public key", p.output) pubkey = p.output fingerprint = get_cert_fingerprint(pubkey) return hexlify(pubkey), fingerprint def get_cert_fingerprint(pubkey): """ Generate a certificate fingerprint the same way keytool does it (but with slightly different formatting) """ digest = hashlib.sha256(pubkey).digest() ret = [' '.join("%02X" % b for b in bytearray(digest))] return " ".join(ret) def get_certificate(signature_block_file): """Extracts a DER certificate from JAR Signature's "Signature Block File". :param signature_block_file: file bytes (as string) representing the certificate, as read directly out of the APK/ZIP :return: A binary representation of the certificate's public key, or None in case of error """ content = decoder.decode(signature_block_file, asn1Spec=rfc2315.ContentInfo())[0] if content.getComponentByName('contentType') != rfc2315.signedData: return None content = decoder.decode(content.getComponentByName('content'), asn1Spec=rfc2315.SignedData())[0] try: certificates = content.getComponentByName('certificates') cert = certificates[0].getComponentByName('certificate') except PyAsn1Error: logging.error("Certificates not found.") return None return encoder.encode(cert) def load_stats_fdroid_signing_key_fingerprints(): """Load list of signing-key fingerprints stored by fdroid publish from file. :returns: list of dictionanryies containing the singing-key fingerprints. """ jar_file = os.path.join('stats', 'publishsigkeys.jar') if not os.path.isfile(jar_file): return {} cmd = [config['jarsigner'], '-strict', '-verify', jar_file] p = FDroidPopen(cmd, output=False) if p.returncode != 4: raise FDroidException("Signature validation of '{}' failed! " "Please run publish again to rebuild this file.".format(jar_file)) jar_sigkey = apk_signer_fingerprint(jar_file) repo_key_sig = config.get('repo_key_sha256') if repo_key_sig: if jar_sigkey != repo_key_sig: raise FDroidException("Signature key fingerprint of file '{}' does not match repo_key_sha256 in config.py (found fingerprint: '{}')".format(jar_file, jar_sigkey)) else: logging.warning("repo_key_sha256 not in config.py, setting it to the signature key fingerprint of '{}'".format(jar_file)) config['repo_key_sha256'] = jar_sigkey write_to_config(config, 'repo_key_sha256') with zipfile.ZipFile(jar_file, 'r') as f: return json.loads(str(f.read('publishsigkeys.json'), 'utf-8')) def write_to_config(thisconfig, key, value=None, config_file=None): '''write a key/value to the local config.py NOTE: only supports writing string variables. :param thisconfig: config dictionary :param key: variable name in config.py to be overwritten/added :param value: optional value to be written, instead of fetched from 'thisconfig' dictionary. ''' if value is None: origkey = key + '_orig' value = thisconfig[origkey] if origkey in thisconfig else thisconfig[key] cfg = config_file if config_file else 'config.py' # load config file, create one if it doesn't exist if not os.path.exists(cfg): open(cfg, 'a').close() logging.info("Creating empty " + cfg) with open(cfg, 'r') as f: lines = f.readlines() # make sure the file ends with a carraige return if len(lines) > 0: if not lines[-1].endswith('\n'): lines[-1] += '\n' # regex for finding and replacing python string variable # definitions/initializations pattern = re.compile(r'^[\s#]*' + key + r'\s*=\s*"[^"]*"') repl = key + ' = "' + value + '"' pattern2 = re.compile(r'^[\s#]*' + key + r"\s*=\s*'[^']*'") repl2 = key + " = '" + value + "'" # If we replaced this line once, we make sure won't be a # second instance of this line for this key in the document. didRepl = False # edit config file with open(cfg, 'w') as f: for line in lines: if pattern.match(line) or pattern2.match(line): if not didRepl: line = pattern.sub(repl, line) line = pattern2.sub(repl2, line) f.write(line) didRepl = True else: f.write(line) if not didRepl: f.write('\n') f.write(repl) f.write('\n') def parse_xml(path): return XMLElementTree.parse(path).getroot() def string_is_integer(string): try: int(string) return True except ValueError: return False def local_rsync(options, fromdir, todir): '''Rsync method for local to local copying of things This is an rsync wrapper with all the settings for safe use within the various fdroidserver use cases. This uses stricter rsync checking on all files since people using offline mode are already prioritizing security above ease and speed. ''' rsyncargs = ['rsync', '--recursive', '--safe-links', '--times', '--perms', '--one-file-system', '--delete', '--chmod=Da+rx,Fa-x,a+r,u+w'] if not options.no_checksum: rsyncargs.append('--checksum') if options.verbose: rsyncargs += ['--verbose'] if options.quiet: rsyncargs += ['--quiet'] logging.debug(' '.join(rsyncargs + [fromdir, todir])) if subprocess.call(rsyncargs + [fromdir, todir]) != 0: raise FDroidException() def deploy_build_log_with_rsync(appid, vercode, log_content, timestamp=int(time.time())): """Upload build log of one individual app build to an fdroid repository. :param appid: package name for dientifying to which app this log belongs. :param vercode: version of the app to which this build belongs. :param log_content: Content of the log which is about to be posted. Should be either a string or bytes. (bytes will be decoded as 'utf-8') :param timestamp: timestamp for avoiding logfile name collisions. """ # check if deploying logs is enabled in config if not config.get('deploy_process_logs', False): logging.debug(_('skip deploying full build logs: not enabled in config')) return if not log_content: logging.warning(_('skip deploying full build logs: log content is empty')) return if not (isinstance(timestamp, int) or isinstance(timestamp, float)): raise ValueError(_("supplied timestamp value '{timestamp}' is not a unix timestamp" .format(timestamp=timestamp))) with tempfile.TemporaryDirectory() as tmpdir: # gzip compress log file log_gz_path = os.path.join( tmpdir, '{pkg}_{ver}_{ts}.log.gz'.format(pkg=appid, ver=vercode, ts=int(timestamp))) with gzip.open(log_gz_path, 'wb') as f: if isinstance(log_content, str): f.write(bytes(log_content, 'utf-8')) else: f.write(log_content) # TODO: sign compressed log file, if a signing key is configured for webroot in config.get('serverwebroot', []): dest_path = os.path.join(webroot, "buildlogs") if not dest_path.endswith('/'): dest_path += '/' # make sure rsync knows this is a directory cmd = ['rsync', '--archive', '--delete-after', '--safe-links'] if options.verbose: cmd += ['--verbose'] if options.quiet: cmd += ['--quiet'] if 'identity_file' in config: cmd += ['-e', 'ssh -oBatchMode=yes -oIdentitiesOnly=yes -i ' + config['identity_file']] cmd += [log_gz_path, dest_path] # TODO: also deploy signature file if present retcode = subprocess.call(cmd) if retcode: logging.warning(_("failed deploying build logs to '{path}'").format(path=webroot)) else: logging.info(_("deployed build logs to '{path}'").format(path=webroot)) def get_per_app_repos(): '''per-app repos are dirs named with the packageName of a single app''' # Android packageNames are Java packages, they may contain uppercase or # lowercase letters ('A' through 'Z'), numbers, and underscores # ('_'). However, individual package name parts may only start with # letters. https://developer.android.com/guide/topics/manifest/manifest-element.html#package p = re.compile('^([a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)*)?$') repos = [] for root, dirs, files in os.walk(os.getcwd()): for d in dirs: print('checking', root, 'for', d) if d in ('archive', 'metadata', 'repo', 'srclibs', 'tmp'): # standard parts of an fdroid repo, so never packageNames continue elif p.match(d) \ and os.path.exists(os.path.join(d, 'fdroid', 'repo', 'index.jar')): repos.append(d) break return repos def is_repo_file(filename): '''Whether the file in a repo is a build product to be delivered to users''' if isinstance(filename, str): filename = filename.encode('utf-8', errors="surrogateescape") return os.path.isfile(filename) \ and not filename.endswith(b'.asc') \ and not filename.endswith(b'.sig') \ and os.path.basename(filename) not in [ b'index.jar', b'index_unsigned.jar', b'index.xml', b'index.html', b'index-v1.jar', b'index-v1.json', b'categories.txt', ] def get_examples_dir(): '''Return the dir where the fdroidserver example files are available''' examplesdir = None tmp = os.path.dirname(sys.argv[0]) if os.path.basename(tmp) == 'bin': egg_links = glob.glob(os.path.join(tmp, '..', 'local/lib/python3.*/site-packages/fdroidserver.egg-link')) if egg_links: # installed from local git repo examplesdir = os.path.join(open(egg_links[0]).readline().rstrip(), 'examples') else: # try .egg layout examplesdir = os.path.dirname(os.path.dirname(__file__)) + '/share/doc/fdroidserver/examples' if not os.path.exists(examplesdir): # use UNIX layout examplesdir = os.path.dirname(tmp) + '/share/doc/fdroidserver/examples' else: # we're running straight out of the git repo prefix = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) examplesdir = prefix + '/examples' return examplesdir def get_wiki_timestamp(timestamp=None): """Return current time in the standard format for posting to the wiki""" if timestamp is None: timestamp = time.gmtime() return time.strftime("%Y-%m-%d %H:%M:%SZ", timestamp) def get_android_tools_versions(ndk_path=None): '''get a list of the versions of all installed Android SDK/NDK components''' global config sdk_path = config['sdk_path'] if sdk_path[-1] != '/': sdk_path += '/' components = [] if ndk_path: ndk_release_txt = os.path.join(ndk_path, 'RELEASE.TXT') if os.path.isfile(ndk_release_txt): with open(ndk_release_txt, 'r') as fp: components.append((os.path.basename(ndk_path), fp.read()[:-1])) pattern = re.compile('^Pkg.Revision=(.+)', re.MULTILINE) for root, dirs, files in os.walk(sdk_path): if 'source.properties' in files: source_properties = os.path.join(root, 'source.properties') with open(source_properties, 'r') as fp: m = pattern.search(fp.read()) if m: components.append((root[len(sdk_path):], m.group(1))) return components def get_android_tools_version_log(ndk_path=None): '''get a list of the versions of all installed Android SDK/NDK components''' log = '== Installed Android Tools ==\n\n' components = get_android_tools_versions(ndk_path) for name, version in sorted(components): log += '* ' + name + ' (' + version + ')\n' return log def get_git_describe_link(): """Get a link to the current fdroiddata commit, to post to the wiki """ try: output = subprocess.check_output(['git', 'describe', '--always', '--dirty', '--abbrev=0'], universal_newlines=True).strip() except subprocess.CalledProcessError: pass if output: commit = output.replace('-dirty', '') return ('* fdroiddata: [https://gitlab.com/fdroid/fdroiddata/commit/{commit} {id}]\n' .format(commit=commit, id=output)) else: logging.error(_("'{path}' failed to execute!").format(path='git describe')) return '' def calculate_math_string(expr): ops = { ast.Add: operator.add, ast.Mult: operator.mul, ast.Sub: operator.sub, ast.USub: operator.neg, } def execute_ast(node): if isinstance(node, ast.Num): # return node.n elif isinstance(node, ast.BinOp): # return ops[type(node.op)](execute_ast(node.left), execute_ast(node.right)) elif isinstance(node, ast.UnaryOp): # e.g., -1 return ops[type(node.op)](ast.literal_eval(node.operand)) else: raise SyntaxError(node) try: if '#' in expr: raise SyntaxError('no comments allowed') return execute_ast(ast.parse(expr, mode='eval').body) except SyntaxError: raise SyntaxError("could not parse expression '{expr}', " "only basic math operations are allowed (+, -, *)" .format(expr=expr)) def force_exit(exitvalue=0): """force exit when thread operations could block the exit The build command has to use some threading stuff to handle the timeout and locks. This seems to prevent the command from exiting, unless this hack is used. """ sys.stdout.flush() sys.stderr.flush() os._exit(exitvalue) fdroidserver-1.1.6/fdroidserver/dscanner.py0000644000175000017500000004137713576156531021016 0ustar hanshans00000000000000#!/usr/bin/env python3 # # dscanner.py - part of the FDroid server tools # Copyright (C) 2016-2017 Shawn Gustaw # # 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 . import logging import os import json import sys from time import sleep from argparse import ArgumentParser from subprocess import CalledProcessError, check_output from . import _ from . import common from . import metadata try: from docker import Client except ImportError: logging.error(("Docker client not installed." "Install it using pip install docker-py")) config = None options = None class DockerConfig: ALIAS = "dscanner" CONTAINER = "dscanner/fdroidserver" EMULATOR = "android-19" ARCH = "armeabi-v7a" class DockerDriver(object): """ Handles all the interactions with the docker container the Android emulator runs in. """ class Commands: build = ['docker', 'build', '--no-cache=false', '--pull=true', '--quiet=false', '--rm=true', '-t', '{0}:latest'.format(DockerConfig.CONTAINER), '.'] run = [ 'docker', 'run', '-e', '"EMULATOR={0}"'.format(DockerConfig.EMULATOR), '-e', '"ARCH={0}"'.format(DockerConfig.ARCH), '-d', '-P', '--name', '{0}'.format(DockerConfig.ALIAS), '--log-driver=json-file', DockerConfig.CONTAINER] start = ['docker', 'start', '{0}'.format(DockerConfig.ALIAS)] inspect = ['docker', 'inspect', '{0}'.format(DockerConfig.ALIAS)] pm_list = 'adb shell "pm list packages"' install_drozer = "docker exec {0} python /home/drozer/install_agent.py" run_drozer = 'python /home/drozer/drozer.py {0}' copy_to_container = 'docker cp "{0}" {1}:{2}' copy_from_container = 'docker cp {0}:{1} "{2}"' def __init__(self, init_only=False, fresh_start=False, clean_only=False): self.container_id = None self.ip_address = None self.cli = Client(base_url='unix://var/run/docker.sock') if fresh_start or clean_only: self.clean() if clean_only: logging.info("Cleaned containers and quitting.") exit(0) self.init_docker() if init_only: logging.info("Initialized and quitting.") exit(0) def _copy_to_container(self, src_path, dest_path): """ Copies a file (presumed to be an apk) from src_path to home directory on container. """ path = '/home/drozer/{path}.apk'.format(path=dest_path) command = self.Commands.copy_to_container.format(src_path, self.container_id, path) try: check_output(command, shell=True) except CalledProcessError as e: logging.error(('Command "{command}" failed with ' 'error code {code}'.format(command=command, code=e.returncode))) raise def _copy_from_container(self, src_path, dest_path): """ Copies a file from src_path on the container to dest_path on the host machine. """ command = self.Commands.copy_from_container.format(self.container_id, src_path, dest_path) try: check_output(command, shell=True) except CalledProcessError as e: logging.error(('Command "{command}" failed with ' 'error code {code}'.format(command=command, code=e.returncode))) raise logging.info("Log stored at {path}".format(path=dest_path)) def _adb_install_apk(self, apk_path): """ Installs an apk on the device running in the container using adb. """ logging.info("Attempting to install an apk.") exec_id = self.cli.exec_create( self.container_id, 'adb install {0}' .format(apk_path) )['Id'] output = self.cli.exec_start(exec_id).decode('utf-8') if "INSTALL_PARSE_FAILED_NO_CERTIFICATES" in output: raise Exception('Install parse failed, no certificates') elif "INSTALL_FAILED_ALREADY_EXISTS" in output: logging.info("APK already installed. Skipping.") elif "Success" not in output: logging.error("APK didn't install properly") return False return True def _adb_uninstall_apk(self, app_id): """ Uninstalls an application from the device running in the container via its app_id. """ logging.info( "Uninstalling {app_id} from the emulator." .format(app_id=app_id) ) exec_id = self.cli.exec_create( self.container_id, 'adb uninstall {0}'.format(app_id) )['Id'] output = self.cli.exec_start(exec_id).decode('utf-8') if 'Success' in output: logging.info("Successfully uninstalled.") return True def _verify_apk_install(self, app_id): """ Checks that the app_id is installed on the device running in the container. """ logging.info( "Verifying {app} is installed on the device." .format(app=app_id) ) exec_id = self.cli.exec_create( self.container_id, self.Commands.pm_list )['Id'] output = self.cli.exec_start(exec_id).decode('utf-8') if ("Could not access the Package Manager" in output or "device offline" in output): logging.info("Device or package manager isn't up") if app_id.split('_')[0] in output: # TODO: this is a temporary fix logging.info("{app} is installed.".format(app=app_id)) return True logging.error("APK not found in packages list on emulator.") def _delete_file(self, path): """ Deletes file off the container to preserve space if scanning many apps """ command = "rm {path}".format(path=path) exec_id = self.cli.exec_create(self.container_id, command)['Id'] logging.info("Deleting {path} on the container.".format(path=path)) self.cli.exec_start(exec_id) def _install_apk(self, apk_path, app_id): """ Installs apk found at apk_path on the emulator. Will then verify it installed properly by looking up its app_id in the package manager. """ if not all([self.container_id, self.ip_address]): # TODO: maybe have this fail nicely raise Exception("Went to install apk and couldn't find container") path = "/home/drozer/{app_id}.apk".format(app_id=app_id) self._copy_to_container(apk_path, app_id) self._adb_install_apk(path) self._verify_apk_install(app_id) self._delete_file(path) def _install_drozer(self): """ Performs all the initialization of drozer within the emulator. """ logging.info("Attempting to install com.mwr.dz on the emulator") logging.info("This could take a while so be patient...") logging.info(("We need to wait for the device to boot AND" " the package manager to come online.")) command = self.Commands.install_drozer.format(self.container_id) try: output = check_output(command, shell=True).decode('utf-8') except CalledProcessError as e: logging.error(('Command "{command}" failed with ' 'error code {code}'.format(command=command, code=e.returncode))) raise if 'Installed ok' in output: return True def _run_drozer_scan(self, app): """ Runs the drozer agent which connects to the app running on the emulator. """ logging.info("Running the drozer agent") exec_id = self.cli.exec_create( self.container_id, self.Commands.run_drozer.format(app) )['Id'] self.cli.exec_start(exec_id) def _container_is_running(self): """ Checks whether the emulator container is running. """ for container in self.cli.containers(): if DockerConfig.ALIAS in container['Image']: return True def _docker_image_exists(self): """ Check whether the docker image exists already. If this returns false we'll need to build the image from the DockerFile. """ for image in self.cli.images(): for tag in image['RepoTags']: if DockerConfig.ALIAS in tag: return True _image_queue = {} def _build_docker_image(self): """ Builds the docker container so we can run the android emulator inside it. """ logging.info("Pulling the container from docker hub") logging.info("Image is roughly 5 GB so be patient") logging.info("(Progress output is slow and requires a tty.)") # we pause briefly to narrow race condition windows of opportunity sleep(1) is_a_tty = os.isatty(sys.stdout.fileno()) for output in self.cli.pull( DockerConfig.CONTAINER, stream=True, tag="latest"): if not is_a_tty: # run silent, run quick continue try: p = json.loads(output.decode('utf-8')) p_id = p['id'] self._image_queue[p_id] = p t, c, j = 1, 1, 0 for k in sorted(self._image_queue): j += 1 v = self._image_queue[k] vd = v['progressDetail'] t += vd['total'] c += vd['current'] msg = "\rDownloading: {0}/{1} {2}% [{3} jobs]" msg = msg.format(c, t, int(c / t * 100), j) sys.stdout.write(msg) sys.stdout.flush() except Exception: pass print("\nDONE!\n") def _verify_apk_exists(self, full_apk_path): """ Verifies that the apk path we have is actually a file. """ return os.path.isfile(full_apk_path) def init_docker(self): """ Perform all the initialization required before a drozer scan. 1. build the image 2. run the container 3. install drozer and enable the service within the app """ built = self._docker_image_exists() if not built: self._build_docker_image() running = self._container_is_running() if not running: logging.info('Trying to run container...') try: check_output(self.Commands.run) except CalledProcessError as e: logging.error(( 'Command "{command}" failed with error code {code}' .format(command=self.Commands.run, code=e.returncode) )) running = self._container_is_running() if not running: logging.info('Trying to start container...') try: check_output(self.Commands.start) except CalledProcessError as e: logging.error(( 'Command "{command}" failed with error code {code}' .format(command=self.Commands.run, code=e.returncode) )) running = self._container_is_running() if not running: raise Exception("Running container not found, critical error.") containers = self.cli.containers() for container in containers: if DockerConfig.ALIAS in container['Image']: self.container_id = container['Id'] n = container['NetworkSettings']['Networks'] self.ip_address = n['bridge']['IPAddress'] break if not self.container_id or not self.ip_address: logging.error("No ip address or container id found.") exit(1) if self._verify_apk_install('com.mwr.dz'): return self._install_drozer() def clean(self): """ Clean up all the containers made by this script. Should be run after the drozer scan completes. """ for container in self.cli.containers(): if DockerConfig.ALIAS in container['Image']: logging.info("Removing container {0}".format(container['Id'])) self.cli.remove_container(container['Id'], force=True) def perform_drozer_scan(self, apk_path, app_id): """ Entrypoint for scanning an android app. Performs the following steps: 1. installs an apk on the device 2. runs a drozer scan 3. copies the report off the container 4. uninstalls the apk to save space on the device """ self._install_apk(apk_path, app_id) logging.info("Running the drozer scan.") self._run_drozer_scan(app_id) logging.info("Scan finished. Moving the report off the container") dest = apk_path + '.drozer' self._copy_from_container('/tmp/drozer_report.log', dest) self._adb_uninstall_apk(app_id) def main(): global config, options # Parse command line... parser = ArgumentParser( usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]" ) common.setup_global_opts(parser) parser.add_argument( "app_id", nargs='*', help=_("applicationId with optional versionCode in the form APPID[:VERCODE]")) parser.add_argument( "-l", "--latest", action="store_true", default=False, help=_("Scan only the latest version of each package")) parser.add_argument( "--clean-after", default=False, action='store_true', help=_("Clean after all scans have finished")) parser.add_argument( "--clean-before", default=False, action='store_true', help=_("Clean before the scans start and rebuild the container")) parser.add_argument( "--clean-only", default=False, action='store_true', help=_("Clean up all containers and then exit")) parser.add_argument( "--init-only", default=False, action='store_true', help=_("Prepare Drozer to run a scan")) parser.add_argument( "--repo-path", default="repo", action="store", help=_("Override path for repo APKs (default: ./repo)")) options = parser.parse_args() config = common.read_config(options) if not os.path.isdir(options.repo_path): sys.stderr.write("repo-path not found: \"" + options.repo_path + "\"") exit(1) # Read all app and srclib metadata allapps = metadata.read_metadata() apps = common.read_app_args(options.app_id, allapps, True) docker = DockerDriver( init_only=options.init_only, fresh_start=options.clean_before, clean_only=options.clean_only ) if options.clean_before: docker.clean() if options.clean_only: exit(0) for app_id, app in apps.items(): vercode = 0 if ':' in app_id: vercode = app_id.split(':')[1] for build in reversed(app.builds): if build.disable: continue if options.latest or vercode == 0 or build.versionCode == vercode: app.builds = [build] break continue continue for app_id, app in apps.items(): for build in app.builds: apks = [] for f in os.listdir(options.repo_path): n = common.get_release_filename(app, build) if f == n: apks.append(f) for apk in sorted(apks): apk_path = os.path.join(options.repo_path, apk) docker.perform_drozer_scan(apk_path, app.id) if options.clean_after: docker.clean() if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/exception.py0000644000175000017500000000215313576156531021204 0ustar hanshans00000000000000class FDroidException(Exception): def __init__(self, value=None, detail=None): self.value = value self.detail = detail def shortened_detail(self): if len(self.detail) < 16000: return self.detail return '[...]\n' + self.detail[-16000:] def get_wikitext(self): ret = repr(self.value) + "\n" if self.detail: ret += "=detail=\n" ret += "
\n" + self.shortened_detail() + "
\n" return ret def __str__(self): if self.value is None: ret = __name__ else: ret = str(self.value) if self.detail: ret += "\n==== detail begin ====\n%s\n==== detail end ====" % ''.join(self.detail).strip() return ret class MetaDataException(Exception): def __init__(self, value): self.value = value def __str__(self): return self.value class VCSException(FDroidException): pass class NoSubmodulesException(VCSException): pass class BuildException(FDroidException): pass class VerificationException(FDroidException): pass fdroidserver-1.1.6/fdroidserver/gpgsign.py0000644000175000017500000000507613576156531020653 0ustar hanshans00000000000000#!/usr/bin/env python3 # # gpgsign.py - part of the FDroid server tools # Copyright (C) 2014, Ciaran Gultnieks, ciaran@ciarang.com # # 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 . import os import glob from argparse import ArgumentParser import logging from . import _ from . import common from .common import FDroidPopen from .exception import FDroidException config = None options = None def main(): global config, options # Parse command line... parser = ArgumentParser(usage="%(prog)s [options]") common.setup_global_opts(parser) options = parser.parse_args() config = common.read_config(options) repodirs = ['repo'] if config['archive_older'] != 0: repodirs.append('archive') for output_dir in repodirs: if not os.path.isdir(output_dir): raise FDroidException(_("Missing output directory") + " '" + output_dir + "'") # Process any apks that are waiting to be signed... for f in sorted(glob.glob(os.path.join(output_dir, '*.*'))): if common.get_file_extension(f) == 'asc': continue if not common.is_repo_file(f): continue filename = os.path.basename(f) sigfilename = filename + ".asc" sigpath = os.path.join(output_dir, sigfilename) if not os.path.exists(sigpath): gpgargs = ['gpg', '-a', '--output', sigpath, '--detach-sig'] if 'gpghome' in config: gpgargs.extend(['--homedir', config['gpghome']]) if 'gpgkey' in config: gpgargs.extend(['--local-user', config['gpgkey']]) gpgargs.append(os.path.join(output_dir, filename)) p = FDroidPopen(gpgargs) if p.returncode != 0: raise FDroidException("Signing failed.") logging.info('Signed ' + filename) if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/import.py0000644000175000017500000002607313576156531020527 0ustar hanshans00000000000000#!/usr/bin/env python3 # # import.py - part of the FDroid server tools # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # # 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 . import binascii import os import re import shutil import urllib.request from argparse import ArgumentParser from configparser import ConfigParser import logging from . import _ from . import common from . import metadata from .exception import FDroidException SETTINGS_GRADLE = re.compile(r'''include\s+['"]:([^'"]*)['"]''') # Get the repo type and address from the given web page. The page is scanned # in a rather naive manner for 'git clone xxxx', 'hg clone xxxx', etc, and # when one of these is found it's assumed that's the information we want. # Returns repotype, address, or None, reason def getrepofrompage(url): req = urllib.request.urlopen(url) if req.getcode() != 200: return (None, 'Unable to get ' + url + ' - return code ' + str(req.getcode())) page = req.read().decode(req.headers.get_content_charset()) # Works for BitBucket m = re.search('data-fetch-url="(.*)"', page) if m is not None: repo = m.group(1) if repo.endswith('.git'): return ('git', repo) return ('hg', repo) # Works for BitBucket (obsolete) index = page.find('hg clone') if index != -1: repotype = 'hg' repo = page[index + 9:] index = repo.find('<') if index == -1: return (None, _("Error while getting repo address")) repo = repo[:index] repo = repo.split('"')[0] return (repotype, repo) # Works for BitBucket (obsolete) index = page.find('git clone') if index != -1: repotype = 'git' repo = page[index + 10:] index = repo.find('<') if index == -1: return (None, _("Error while getting repo address")) repo = repo[:index] repo = repo.split('"')[0] return (repotype, repo) return (None, _("No information found.") + page) config = None options = None def get_metadata_from_url(app, url): tmp_dir = 'tmp' if not os.path.isdir(tmp_dir): logging.info(_("Creating temporary directory")) os.makedirs(tmp_dir) # Figure out what kind of project it is... projecttype = None app.WebSite = url # by default, we might override it if url.startswith('git://'): projecttype = 'git' repo = url repotype = 'git' app.SourceCode = "" app.WebSite = "" elif url.startswith('https://github.com'): projecttype = 'github' repo = url repotype = 'git' app.SourceCode = url app.IssueTracker = url + '/issues' app.WebSite = "" elif url.startswith('https://gitlab.com/'): projecttype = 'gitlab' # git can be fussy with gitlab URLs unless they end in .git if url.endswith('.git'): url = url[:-4] repo = url + '.git' repotype = 'git' app.WebSite = url app.SourceCode = url + '/tree/HEAD' app.IssueTracker = url + '/issues' elif url.startswith('https://notabug.org/'): projecttype = 'notabug' if url.endswith('.git'): url = url[:-4] repo = url + '.git' repotype = 'git' app.SourceCode = url app.IssueTracker = url + '/issues' app.WebSite = "" elif url.startswith('https://bitbucket.org/'): if url.endswith('/'): url = url[:-1] projecttype = 'bitbucket' app.SourceCode = url + '/src' app.IssueTracker = url + '/issues' # Figure out the repo type and adddress... repotype, repo = getrepofrompage(url) if not repotype: raise FDroidException("Unable to determine vcs type. " + repo) elif url.startswith('https://') and url.endswith('.git'): projecttype = 'git' repo = url repotype = 'git' app.SourceCode = "" app.WebSite = "" if not projecttype: raise FDroidException("Unable to determine the project type. " + "The URL you supplied was not in one of the supported formats. " + "Please consult the manual for a list of supported formats, " + "and supply one of those.") # Ensure we have a sensible-looking repo address at this point. If not, we # might have got a page format we weren't expecting. (Note that we # specifically don't want git@...) if ((repotype != 'bzr' and (not repo.startswith('http://') and not repo.startswith('https://') and not repo.startswith('git://'))) or ' ' in repo): raise FDroidException("Repo address '{0}' does not seem to be valid".format(repo)) # Get a copy of the source so we can extract some info... logging.info('Getting source from ' + repotype + ' repo at ' + repo) build_dir = os.path.join(tmp_dir, 'importer') if os.path.exists(build_dir): shutil.rmtree(build_dir) vcs = common.getvcs(repotype, repo, build_dir) vcs.gotorevision(options.rev) root_dir = get_subdir(build_dir) app.RepoType = repotype app.Repo = repo return root_dir, build_dir config = None options = None def get_subdir(build_dir): if options.subdir: return os.path.join(build_dir, options.subdir) settings_gradle = os.path.join(build_dir, 'settings.gradle') if os.path.exists(settings_gradle): with open(settings_gradle) as fp: m = SETTINGS_GRADLE.search(fp.read()) if m: return os.path.join(build_dir, m.group(1)) return build_dir def main(): global config, options # Parse command line... parser = ArgumentParser() common.setup_global_opts(parser) parser.add_argument("-u", "--url", default=None, help=_("Project URL to import from.")) parser.add_argument("-s", "--subdir", default=None, help=_("Path to main Android project subdirectory, if not in root.")) parser.add_argument("-c", "--categories", default=None, help=_("Comma separated list of categories.")) parser.add_argument("-l", "--license", default=None, help=_("Overall license of the project.")) parser.add_argument("--rev", default=None, help=_("Allows a different revision (or git branch) to be specified for the initial import")) metadata.add_metadata_arguments(parser) options = parser.parse_args() metadata.warnings_action = options.W config = common.read_config(options) apps = metadata.read_metadata() app = metadata.App() app.UpdateCheckMode = "Tags" root_dir = None build_dir = None local_metadata_files = common.get_local_metadata_files() if local_metadata_files != []: raise FDroidException(_("This repo already has local metadata: %s") % local_metadata_files[0]) build = metadata.Build() if options.url is None and os.path.isdir('.git'): app.AutoName = os.path.basename(os.getcwd()) app.RepoType = 'git' root_dir = get_subdir(os.getcwd()) if os.path.exists('build.gradle'): build.gradle = ['yes'] import git repo = git.repo.Repo(root_dir) # git repo for remote in git.Remote.iter_items(repo): if remote.name == 'origin': url = repo.remotes.origin.url if url.startswith('https://git'): # github, gitlab app.SourceCode = url.rstrip('.git') app.Repo = url break # repo.head.commit.binsha is a bytearray stored in a str build.commit = binascii.hexlify(bytearray(repo.head.commit.binsha)) write_local_file = True elif options.url: root_dir, build_dir = get_metadata_from_url(app, options.url) build.commit = '?' build.disable = 'Generated by import.py - check/set version fields and commit id' write_local_file = False else: raise FDroidException("Specify project url.") # Extract some information... paths = common.manifest_paths(root_dir, []) if paths: versionName, versionCode, package = common.parse_androidmanifests(paths, app) if not package: raise FDroidException(_("Couldn't find package ID")) if not versionName: logging.warn(_("Couldn't find latest version name")) if not versionCode: logging.warn(_("Couldn't find latest version code")) else: spec = os.path.join(root_dir, 'buildozer.spec') if os.path.exists(spec): defaults = {'orientation': 'landscape', 'icon': '', 'permissions': '', 'android.api': "18"} bconfig = ConfigParser(defaults, allow_no_value=True) bconfig.read(spec) package = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name') versionName = bconfig.get('app', 'version') versionCode = None else: raise FDroidException(_("No android or kivy project could be found. Specify --subdir?")) # Make sure it's actually new... if package in apps: raise FDroidException("Package " + package + " already exists") # Create a build line... build.versionName = versionName or 'Unknown' build.versionCode = versionCode or '0' # TODO heinous but this is still a str if options.subdir: build.subdir = options.subdir if options.license: app.License = options.license if options.categories: app.Categories = options.categories.split(',') if os.path.exists(os.path.join(root_dir, 'jni')): build.buildjni = ['yes'] if os.path.exists(os.path.join(root_dir, 'build.gradle')): build.gradle = ['yes'] metadata.post_metadata_parse(app) app.builds.append(build) if write_local_file: metadata.write_metadata('.fdroid.yml', app) else: # Keep the repo directory to save bandwidth... if not os.path.exists('build'): os.mkdir('build') if build_dir is not None: shutil.move(build_dir, os.path.join('build', package)) with open('build/.fdroidvcs-' + package, 'w') as f: f.write(app.RepoType + ' ' + app.Repo) metadatapath = os.path.join('metadata', package + '.yml') metadata.write_metadata(metadatapath, app) logging.info("Wrote " + metadatapath) if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/index.py0000644000175000017500000007673213576156546020341 0ustar hanshans00000000000000#!/usr/bin/env python3 # # update.py - part of the FDroid server tools # Copyright (C) 2017, Torsten Grote # Copyright (C) 2016, Blue Jay Wireless # Copyright (C) 2014-2016, Hans-Christoph Steiner # Copyright (C) 2010-2015, Ciaran Gultnieks # Copyright (C) 2013-2014, Daniel Martí # # 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 . import collections import copy import json import logging import os import re import shutil import tempfile import urllib.parse import zipfile import calendar from binascii import hexlify, unhexlify from datetime import datetime, timezone from xml.dom.minidom import Document from . import _ from . import common from . import metadata from . import net from . import signindex from fdroidserver.common import FDroidPopen, FDroidPopenBytes, load_stats_fdroid_signing_key_fingerprints from fdroidserver.exception import FDroidException, VerificationException, MetaDataException def make(apps, sortedids, apks, repodir, archive): """Generate the repo index files. This requires properly initialized options and config objects. :param apps: fully populated apps list :param sortedids: app package IDs, sorted :param apks: full populated apks list :param repodir: the repo directory :param archive: True if this is the archive repo, False if it's the main one. """ from fdroidserver.update import METADATA_VERSION def _resolve_description_link(appid): if appid in apps: return "fdroid.app:" + appid, apps[appid].Name raise MetaDataException("Cannot resolve app id " + appid) if not common.options.nosign: common.assert_config_keystore(common.config) repodict = collections.OrderedDict() repodict['timestamp'] = datetime.utcnow().replace(tzinfo=timezone.utc) repodict['version'] = METADATA_VERSION if common.config['repo_maxage'] != 0: repodict['maxage'] = common.config['repo_maxage'] if archive: repodict['name'] = common.config['archive_name'] repodict['icon'] = os.path.basename(common.config['archive_icon']) repodict['address'] = common.config['archive_url'] repodict['description'] = common.config['archive_description'] urlbasepath = os.path.basename(urllib.parse.urlparse(common.config['archive_url']).path) else: repodict['name'] = common.config['repo_name'] repodict['icon'] = os.path.basename(common.config['repo_icon']) repodict['address'] = common.config['repo_url'] repodict['description'] = common.config['repo_description'] urlbasepath = os.path.basename(urllib.parse.urlparse(common.config['repo_url']).path) mirrorcheckfailed = False mirrors = [] for mirror in sorted(common.config.get('mirrors', [])): base = os.path.basename(urllib.parse.urlparse(mirror).path.rstrip('/')) if common.config.get('nonstandardwebroot') is not True and base != 'fdroid': logging.error(_("mirror '%s' does not end with 'fdroid'!") % mirror) mirrorcheckfailed = True # must end with / or urljoin strips a whole path segment if mirror.endswith('/'): mirrors.append(urllib.parse.urljoin(mirror, urlbasepath)) else: mirrors.append(urllib.parse.urljoin(mirror + '/', urlbasepath)) for mirror in common.config.get('servergitmirrors', []): for url in get_mirror_service_urls(mirror): mirrors.append(url + '/' + repodir) if mirrorcheckfailed: raise FDroidException(_("Malformed repository mirrors.")) if mirrors: repodict['mirrors'] = mirrors appsWithPackages = collections.OrderedDict() for packageName in sortedids: app = apps[packageName] if app['Disabled']: continue # only include apps with packages for apk in apks: if apk['packageName'] == packageName: newapp = copy.copy(app) # update wiki needs unmodified description newapp['Description'] = metadata.description_html(app['Description'], _resolve_description_link) appsWithPackages[packageName] = newapp break requestsdict = collections.OrderedDict() for command in ('install', 'uninstall'): packageNames = [] key = command + '_list' if key in common.config: if isinstance(common.config[key], str): packageNames = [common.config[key]] elif all(isinstance(item, str) for item in common.config[key]): packageNames = common.config[key] else: raise TypeError(_('only accepts strings, lists, and tuples')) requestsdict[command] = packageNames fdroid_signing_key_fingerprints = load_stats_fdroid_signing_key_fingerprints() make_v0(appsWithPackages, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints) make_v1(appsWithPackages, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints) def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints): def _index_encoder_default(obj): if isinstance(obj, set): return sorted(list(obj)) if isinstance(obj, datetime): # Java prefers milliseconds # we also need to accound for time zone/daylight saving time return int(calendar.timegm(obj.timetuple()) * 1000) if isinstance(obj, dict): d = collections.OrderedDict() for key in sorted(obj.keys()): d[key] = obj[key] return d raise TypeError(repr(obj) + " is not JSON serializable") output = collections.OrderedDict() output['repo'] = repodict output['requests'] = requestsdict # establish sort order of the index v1_sort_packages(packages, fdroid_signing_key_fingerprints) appslist = [] output['apps'] = appslist for packageName, appdict in apps.items(): d = collections.OrderedDict() appslist.append(d) for k, v in sorted(appdict.items()): if not v: continue if k in ('builds', 'comments', 'metadatapath', 'ArchivePolicy', 'AutoUpdateMode', 'MaintainerNotes', 'Provides', 'Repo', 'RepoType', 'RequiresRoot', 'UpdateCheckData', 'UpdateCheckIgnore', 'UpdateCheckMode', 'UpdateCheckName', 'NoSourceSince', 'VercodeOperation'): continue # name things after the App class fields in fdroidclient if k == 'id': k = 'packageName' elif k == 'CurrentVersionCode': # TODO make SuggestedVersionCode the canonical name k = 'suggestedVersionCode' elif k == 'CurrentVersion': # TODO make SuggestedVersionName the canonical name k = 'suggestedVersionName' elif k == 'AutoName': if 'Name' not in apps[packageName]: d['name'] = v continue else: k = k[:1].lower() + k[1:] d[k] = v # establish sort order in localized dicts for app in output['apps']: localized = app.get('localized') if localized: lordered = collections.OrderedDict() for lkey, lvalue in sorted(localized.items()): lordered[lkey] = collections.OrderedDict() for ikey, iname in sorted(lvalue.items()): lordered[lkey][ikey] = iname app['localized'] = lordered output_packages = collections.OrderedDict() output['packages'] = output_packages for package in packages: packageName = package['packageName'] if packageName not in apps: logging.info(_('Ignoring package without metadata: ') + package['apkName']) continue if not package.get('versionName'): app = apps[packageName] versionCodeStr = str(package['versionCode']) # TODO build.versionCode should be int! for build in app['builds']: if build['versionCode'] == versionCodeStr: versionName = build.get('versionName') logging.info(_('Overriding blank versionName in {apkfilename} from metadata: {version}') .format(apkfilename=package['apkName'], version=versionName)) package['versionName'] = versionName break if packageName in output_packages: packagelist = output_packages[packageName] else: packagelist = [] output_packages[packageName] = packagelist d = collections.OrderedDict() packagelist.append(d) for k, v in sorted(package.items()): if not v: continue if k in ('icon', 'icons', 'icons_src', 'name', ): continue d[k] = v json_name = 'index-v1.json' index_file = os.path.join(repodir, json_name) with open(index_file, 'w') as fp: if common.options.pretty: json.dump(output, fp, default=_index_encoder_default, indent=2) else: json.dump(output, fp, default=_index_encoder_default) if common.options.nosign: logging.debug(_('index-v1 must have a signature, use `fdroid signindex` to create it!')) else: signindex.config = common.config signindex.sign_index_v1(repodir, json_name) def v1_sort_packages(packages, fdroid_signing_key_fingerprints): """Sorts the supplied list to ensure a deterministic sort order for package entries in the index file. This sort-order also expresses installation preference to the clients. (First in this list = first to install) :param packages: list of packages which need to be sorted before but into index file. """ GROUP_DEV_SIGNED = 1 GROUP_FDROID_SIGNED = 2 GROUP_OTHER_SIGNED = 3 def v1_sort_keys(package): packageName = package.get('packageName', None) sig = package.get('signer', None) dev_sig = common.metadata_find_developer_signature(packageName) group = GROUP_OTHER_SIGNED if dev_sig and dev_sig == sig: group = GROUP_DEV_SIGNED else: fdroidsig = fdroid_signing_key_fingerprints.get(packageName, {}).get('signer') if fdroidsig and fdroidsig == sig: group = GROUP_FDROID_SIGNED versionCode = None if package.get('versionCode', None): versionCode = -int(package['versionCode']) return(packageName, group, sig, versionCode) packages.sort(key=v1_sort_keys) def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints): """ aka index.jar aka index.xml """ doc = Document() def addElement(name, value, doc, parent): el = doc.createElement(name) el.appendChild(doc.createTextNode(value)) parent.appendChild(el) def addElementNonEmpty(name, value, doc, parent): if not value: return addElement(name, value, doc, parent) def addElementIfInApk(name, apk, key, doc, parent): if key not in apk: return value = str(apk[key]) addElement(name, value, doc, parent) def addElementCDATA(name, value, doc, parent): el = doc.createElement(name) el.appendChild(doc.createCDATASection(value)) parent.appendChild(el) def addElementCheckLocalized(name, app, key, doc, parent, default=''): '''Fill in field from metadata or localized block For name/summary/description, they can come only from the app source, or from a dir in fdroiddata. They can be entirely missing from the metadata file if there is localized versions. This will fetch those from the localized version if its not available in the metadata file. ''' el = doc.createElement(name) value = app.get(key) lkey = key[:1].lower() + key[1:] localized = app.get('localized') if not value and localized: for lang in ['en-US'] + [x for x in localized.keys()]: if not lang.startswith('en'): continue if lang in localized: value = localized[lang].get(lkey) if value: break if not value and localized and len(localized) > 1: lang = list(localized.keys())[0] value = localized[lang].get(lkey) if not value: value = default el.appendChild(doc.createTextNode(value)) parent.appendChild(el) root = doc.createElement("fdroid") doc.appendChild(root) repoel = doc.createElement("repo") repoel.setAttribute("name", repodict['name']) if 'maxage' in repodict: repoel.setAttribute("maxage", str(repodict['maxage'])) repoel.setAttribute("icon", os.path.basename(repodict['icon'])) repoel.setAttribute("url", repodict['address']) addElement('description', repodict['description'], doc, repoel) for mirror in repodict.get('mirrors', []): addElement('mirror', mirror, doc, repoel) repoel.setAttribute("version", str(repodict['version'])) repoel.setAttribute("timestamp", '%d' % repodict['timestamp'].timestamp()) pubkey, repo_pubkey_fingerprint = extract_pubkey() repoel.setAttribute("pubkey", pubkey.decode('utf-8')) root.appendChild(repoel) for command in ('install', 'uninstall'): for packageName in requestsdict[command]: element = doc.createElement(command) root.appendChild(element) element.setAttribute('packageName', packageName) for appid, appdict in apps.items(): app = metadata.App(appdict) if app.Disabled is not None: continue # Get a list of the apks for this app... apklist = [] apksbyversion = collections.defaultdict(lambda: []) for apk in apks: if apk.get('versionCode') and apk.get('packageName') == appid: apksbyversion[apk['versionCode']].append(apk) for versionCode, apksforver in apksbyversion.items(): fdroidsig = fdroid_signing_key_fingerprints.get(appid, {}).get('signer') fdroid_signed_apk = None name_match_apk = None for x in apksforver: if fdroidsig and x.get('signer', None) == fdroidsig: fdroid_signed_apk = x if common.apk_release_filename.match(x.get('apkName', '')): name_match_apk = x # choose which of the available versions is most # suiteable for index v0 if fdroid_signed_apk: apklist.append(fdroid_signed_apk) elif name_match_apk: apklist.append(name_match_apk) else: apklist.append(apksforver[0]) if len(apklist) == 0: continue apel = doc.createElement("application") apel.setAttribute("id", app.id) root.appendChild(apel) addElement('id', app.id, doc, apel) if app.added: addElement('added', app.added.strftime('%Y-%m-%d'), doc, apel) if app.lastUpdated: addElement('lastupdated', app.lastUpdated.strftime('%Y-%m-%d'), doc, apel) addElementCheckLocalized('name', app, 'Name', doc, apel) addElementCheckLocalized('summary', app, 'Summary', doc, apel) if app.icon: addElement('icon', app.icon, doc, apel) addElementCheckLocalized('desc', app, 'Description', doc, apel, '

No description available

') addElement('license', app.License, doc, apel) if app.Categories: addElement('categories', ','.join(app.Categories), doc, apel) # We put the first (primary) category in LAST, which will have # the desired effect of making clients that only understand one # category see that one. addElement('category', app.Categories[0], doc, apel) addElement('web', app.WebSite, doc, apel) addElement('source', app.SourceCode, doc, apel) addElement('tracker', app.IssueTracker, doc, apel) addElementNonEmpty('changelog', app.Changelog, doc, apel) addElementNonEmpty('author', app.AuthorName, doc, apel) addElementNonEmpty('email', app.AuthorEmail, doc, apel) addElementNonEmpty('donate', app.Donate, doc, apel) addElementNonEmpty('bitcoin', app.Bitcoin, doc, apel) addElementNonEmpty('litecoin', app.Litecoin, doc, apel) addElementNonEmpty('flattr', app.FlattrID, doc, apel) addElementNonEmpty('liberapay', app.LiberapayID, doc, apel) # These elements actually refer to the current version (i.e. which # one is recommended. They are historically mis-named, and need # changing, but stay like this for now to support existing clients. addElement('marketversion', app.CurrentVersion, doc, apel) addElement('marketvercode', app.CurrentVersionCode, doc, apel) if app.Provides: pv = app.Provides.split(',') addElementNonEmpty('provides', ','.join(pv), doc, apel) if app.RequiresRoot: addElement('requirements', 'root', doc, apel) # Sort the apk list into version order, just so the web site # doesn't have to do any work by default... apklist = sorted(apklist, key=lambda apk: apk['versionCode'], reverse=True) if 'antiFeatures' in apklist[0]: app.AntiFeatures.extend(apklist[0]['antiFeatures']) if app.AntiFeatures: addElementNonEmpty('antifeatures', ','.join(app.AntiFeatures), doc, apel) # Check for duplicates - they will make the client unhappy... for i in range(len(apklist) - 1): first = apklist[i] second = apklist[i + 1] if first['versionCode'] == second['versionCode'] \ and first['sig'] == second['sig']: if first['hash'] == second['hash']: raise FDroidException('"{0}/{1}" and "{0}/{2}" are exact duplicates!'.format( repodir, first['apkName'], second['apkName'])) else: raise FDroidException('duplicates: "{0}/{1}" - "{0}/{2}"'.format( repodir, first['apkName'], second['apkName'])) current_version_code = 0 current_version_file = None for apk in apklist: file_extension = common.get_file_extension(apk['apkName']) # find the APK for the "Current Version" if current_version_code < apk['versionCode']: current_version_code = apk['versionCode'] if current_version_code < int(app.CurrentVersionCode): current_version_file = apk['apkName'] apkel = doc.createElement("package") apel.appendChild(apkel) versionName = apk.get('versionName') if not versionName: versionCodeStr = str(apk['versionCode']) # TODO build.versionCode should be int! for build in app.builds: if build['versionCode'] == versionCodeStr and 'versionName' in build: versionName = build['versionName'] break if versionName: addElement('version', versionName, doc, apkel) addElement('versioncode', str(apk['versionCode']), doc, apkel) addElement('apkname', apk['apkName'], doc, apkel) addElementIfInApk('srcname', apk, 'srcname', doc, apkel) hashel = doc.createElement("hash") hashel.setAttribute('type', 'sha256') hashel.appendChild(doc.createTextNode(apk['hash'])) apkel.appendChild(hashel) addElement('size', str(apk['size']), doc, apkel) addElementIfInApk('sdkver', apk, 'minSdkVersion', doc, apkel) addElementIfInApk('targetSdkVersion', apk, 'targetSdkVersion', doc, apkel) addElementIfInApk('maxsdkver', apk, 'maxSdkVersion', doc, apkel) addElementIfInApk('obbMainFile', apk, 'obbMainFile', doc, apkel) addElementIfInApk('obbMainFileSha256', apk, 'obbMainFileSha256', doc, apkel) addElementIfInApk('obbPatchFile', apk, 'obbPatchFile', doc, apkel) addElementIfInApk('obbPatchFileSha256', apk, 'obbPatchFileSha256', doc, apkel) if 'added' in apk: addElement('added', apk['added'].strftime('%Y-%m-%d'), doc, apkel) if file_extension == 'apk': # sig is required for APKs, but only APKs addElement('sig', apk['sig'], doc, apkel) old_permissions = set() sorted_permissions = sorted(apk['uses-permission']) for perm in sorted_permissions: perm_name = perm[0] if perm_name.startswith("android.permission."): perm_name = perm_name[19:] old_permissions.add(perm_name) addElementNonEmpty('permissions', ','.join(sorted(old_permissions)), doc, apkel) for permission in sorted_permissions: permel = doc.createElement('uses-permission') permel.setAttribute('name', permission[0]) if permission[1] is not None: permel.setAttribute('maxSdkVersion', '%d' % permission[1]) apkel.appendChild(permel) for permission_sdk_23 in sorted(apk['uses-permission-sdk-23']): permel = doc.createElement('uses-permission-sdk-23') permel.setAttribute('name', permission_sdk_23[0]) if permission_sdk_23[1] is not None: permel.setAttribute('maxSdkVersion', '%d' % permission_sdk_23[1]) apkel.appendChild(permel) if 'nativecode' in apk: addElement('nativecode', ','.join(sorted(apk['nativecode'])), doc, apkel) addElementNonEmpty('features', ','.join(sorted(apk['features'])), doc, apkel) if current_version_file is not None \ and common.config['make_current_version_link'] \ and repodir == 'repo': # only create these namefield = common.config['current_version_name_source'] sanitized_name = re.sub(b'''[ '"&%?+=/]''', b'', app.get(namefield).encode('utf-8')) apklinkname = sanitized_name + os.path.splitext(current_version_file)[1].encode('utf-8') current_version_path = os.path.join(repodir, current_version_file).encode('utf-8', 'surrogateescape') if os.path.islink(apklinkname): os.remove(apklinkname) os.symlink(current_version_path, apklinkname) # also symlink gpg signature, if it exists for extension in (b'.asc', b'.sig'): sigfile_path = current_version_path + extension if os.path.exists(sigfile_path): siglinkname = apklinkname + extension if os.path.islink(siglinkname): os.remove(siglinkname) os.symlink(sigfile_path, siglinkname) if common.options.pretty: output = doc.toprettyxml(encoding='utf-8') else: output = doc.toxml(encoding='utf-8') with open(os.path.join(repodir, 'index.xml'), 'wb') as f: f.write(output) if 'repo_keyalias' in common.config: if common.options.nosign: logging.info(_("Creating unsigned index in preparation for signing")) else: logging.info(_("Creating signed index with this key (SHA256):")) logging.info("%s" % repo_pubkey_fingerprint) # Create a jar of the index... jar_output = 'index_unsigned.jar' if common.options.nosign else 'index.jar' p = FDroidPopen(['jar', 'cf', jar_output, 'index.xml'], cwd=repodir) if p.returncode != 0: raise FDroidException("Failed to create {0}".format(jar_output)) # Sign the index... signed = os.path.join(repodir, 'index.jar') if common.options.nosign: # Remove old signed index if not signing if os.path.exists(signed): os.remove(signed) else: signindex.config = common.config signindex.sign_jar(signed) # Copy the repo icon into the repo directory... icon_dir = os.path.join(repodir, 'icons') iconfilename = os.path.join(icon_dir, os.path.basename(common.config['repo_icon'])) shutil.copyfile(common.config['repo_icon'], iconfilename) def extract_pubkey(): """ Extracts and returns the repository's public key from the keystore. :return: public key in hex, repository fingerprint """ if 'repo_pubkey' in common.config: pubkey = unhexlify(common.config['repo_pubkey']) else: env_vars = {'LC_ALL': 'C.UTF-8', 'FDROID_KEY_STORE_PASS': common.config['keystorepass']} p = FDroidPopenBytes([common.config['keytool'], '-exportcert', '-alias', common.config['repo_keyalias'], '-keystore', common.config['keystore'], '-storepass:env', 'FDROID_KEY_STORE_PASS'] + common.config['smartcardoptions'], envs=env_vars, output=False, stderr_to_stdout=False) if p.returncode != 0 or len(p.output) < 20: msg = "Failed to get repo pubkey!" if common.config['keystore'] == 'NONE': msg += ' Is your crypto smartcard plugged in?' raise FDroidException(msg) pubkey = p.output repo_pubkey_fingerprint = common.get_cert_fingerprint(pubkey) return hexlify(pubkey), repo_pubkey_fingerprint def get_mirror_service_urls(url): '''Get direct URLs from git service for use by fdroidclient Via 'servergitmirrors', fdroidserver can create and push a mirror to certain well known git services like gitlab or github. This will always use the 'master' branch since that is the default branch in git. The files are then accessible via alternate URLs, where they are served in their raw format via a CDN rather than from git. ''' if url.startswith('git@'): url = re.sub(r'^git@(.*):(.*)', r'https://\1/\2', url) segments = url.split("/") if segments[4].endswith('.git'): segments[4] = segments[4][:-4] hostname = segments[2] user = segments[3] repo = segments[4] branch = "master" folder = "fdroid" urls = [] if hostname == "github.com": # Github-like RAW segments "https://raw.githubusercontent.com/user/repo/branch/folder" segments[2] = "raw.githubusercontent.com" segments.extend([branch, folder]) urls.append('/'.join(segments)) elif hostname == "gitlab.com": # Both these Gitlab URLs will work with F-Droid, but only the first will work in the browser # This is because the `raw` URLs are not served with the correct mime types, so any # index.html which is put in the repo will not be rendered. Putting an index.html file in # the repo root is a common way for to make information about the repo available to end user. # Gitlab-like Pages segments "https://user.gitlab.io/repo/folder" gitlab_pages = ["https:", "", user + ".gitlab.io", repo, folder] urls.append('/'.join(gitlab_pages)) # Gitlab Raw "https://gitlab.com/user/repo/raw/branch/folder" gitlab_raw = segments + ['raw', branch, folder] urls.append('/'.join(gitlab_raw)) return urls return urls def download_repo_index(url_str, etag=None, verify_fingerprint=True, timeout=600): """Downloads and verifies index file, then returns its data. Downloads the repository index from the given :param url_str and verifies the repository's fingerprint if :param verify_fingerprint is not False. :raises: VerificationException() if the repository could not be verified :return: A tuple consisting of: - The index in JSON format or None if the index did not change - The new eTag as returned by the HTTP request """ url = urllib.parse.urlsplit(url_str) fingerprint = None if verify_fingerprint: query = urllib.parse.parse_qs(url.query) if 'fingerprint' not in query: raise VerificationException(_("No fingerprint in URL.")) fingerprint = query['fingerprint'][0] url = urllib.parse.SplitResult(url.scheme, url.netloc, url.path + '/index-v1.jar', '', '') download, new_etag = net.http_get(url.geturl(), etag, timeout) if download is None: return None, new_etag with tempfile.NamedTemporaryFile() as fp: fp.write(download) fp.flush() index, public_key, public_key_fingerprint = get_index_from_jar(fp.name, fingerprint) index["repo"]["pubkey"] = hexlify(public_key).decode() index["repo"]["fingerprint"] = public_key_fingerprint index["apps"] = [metadata.App(app) for app in index["apps"]] return index, new_etag def get_index_from_jar(jarfile, fingerprint=None): """Returns the data, public key, and fingerprint from index-v1.jar :raises: VerificationException() if the repository could not be verified """ logging.debug(_('Verifying index signature:')) common.verify_jar_signature(jarfile) with zipfile.ZipFile(jarfile) as jar: public_key, public_key_fingerprint = get_public_key_from_jar(jar) if fingerprint is not None: if fingerprint.upper() != public_key_fingerprint: raise VerificationException(_("The repository's fingerprint does not match.")) data = json.loads(jar.read('index-v1.json').decode()) return data, public_key, public_key_fingerprint def get_public_key_from_jar(jar): """ Get the public key and its fingerprint from a JAR file. :raises: VerificationException() if the JAR was not signed exactly once :param jar: a zipfile.ZipFile object :return: the public key from the jar and its fingerprint """ # extract certificate from jar certs = [n for n in jar.namelist() if common.SIGNATURE_BLOCK_FILE_REGEX.match(n)] if len(certs) < 1: raise VerificationException(_("Found no signing certificates for repository.")) if len(certs) > 1: raise VerificationException(_("Found multiple signing certificates for repository.")) # extract public key from certificate public_key = common.get_certificate(jar.read(certs[0])) public_key_fingerprint = common.get_cert_fingerprint(public_key).replace(' ', '') return public_key, public_key_fingerprint fdroidserver-1.1.6/fdroidserver/init.py0000644000175000017500000002732713576156531020163 0ustar hanshans00000000000000#!/usr/bin/env python3 # # init.py - part of the FDroid server tools # Copyright (C) 2010-2013, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # Copyright (C) 2013 Hans-Christoph Steiner # # 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 . import glob import os import re import shutil import socket import sys from argparse import ArgumentParser import logging from . import _ from . import common from .exception import FDroidException config = {} options = None def disable_in_config(key, value): '''write a key/value to the local config.py, then comment it out''' with open('config.py', 'r') as f: data = f.read() pattern = r'\n[\s#]*' + key + r'\s*=\s*"[^"]*"' repl = '\n#' + key + ' = "' + value + '"' data = re.sub(pattern, repl, data) with open('config.py', 'w') as f: f.writelines(data) def main(): global options, config # Parse command line... parser = ArgumentParser() common.setup_global_opts(parser) parser.add_argument("-d", "--distinguished-name", default=None, help=_("X.509 'Distinguished Name' used when generating keys")) parser.add_argument("--keystore", default=None, help=_("Path to the keystore for the repo signing key")) parser.add_argument("--repo-keyalias", default=None, help=_("Alias of the repo signing key in the keystore")) parser.add_argument("--android-home", default=None, help=_("Path to the Android SDK (sometimes set in ANDROID_HOME)")) parser.add_argument("--no-prompt", action="store_true", default=False, help=_("Do not prompt for Android SDK path, just fail")) options = parser.parse_args() aapt = None fdroiddir = os.getcwd() test_config = dict() examplesdir = common.get_examples_dir() common.fill_config_defaults(test_config) # track down where the Android SDK is, the default is to use the path set # in ANDROID_HOME if that exists, otherwise None if options.android_home is not None: test_config['sdk_path'] = options.android_home elif common.use_androguard(): pass elif not common.test_sdk_exists(test_config): if os.path.isfile('/usr/bin/aapt'): # remove sdk_path and build_tools, they are not required test_config.pop('sdk_path', None) test_config.pop('build_tools', None) # make sure at least aapt is found, since this can't do anything without it test_config['aapt'] = common.find_sdk_tools_cmd('aapt') else: # if neither --android-home nor the default sdk_path # exist, prompt the user using platform-specific default default_sdk_path = '/opt/android-sdk' if sys.platform == 'win32' or sys.platform == 'cygwin': p = os.path.join(os.getenv('USERPROFILE'), 'AppData', 'Local', 'Android', 'android-sdk') elif sys.platform == 'darwin': # on OSX, Homebrew is common and has an easy path to detect p = '/usr/local/opt/android-sdk' else: # if the Debian packages are installed, suggest them p = '/usr/lib/android-sdk' if os.path.exists(p): default_sdk_path = p while not options.no_prompt: try: s = input(_('Enter the path to the Android SDK (%s) here:\n> ') % default_sdk_path) except KeyboardInterrupt: print('') sys.exit(1) if re.match(r'^\s*$', s) is not None: test_config['sdk_path'] = default_sdk_path else: test_config['sdk_path'] = s if common.test_sdk_exists(test_config): break if (options.android_home is not None or not common.use_androguard()) \ and not common.test_sdk_exists(test_config): raise FDroidException("Android SDK not found.") if not os.path.exists('config.py'): # 'metadata' and 'tmp' are created in fdroid if not os.path.exists('repo'): os.mkdir('repo') shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir) shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py') os.chmod('config.py', 0o0600) # If android_home is None, test_config['sdk_path'] will be used and # "$ANDROID_HOME" may be used if the env var is set up correctly. # If android_home is not None, the path given from the command line # will be directly written in the config. if 'sdk_path' in test_config: common.write_to_config(test_config, 'sdk_path', options.android_home) else: logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...') logging.info('Try running `fdroid init` in an empty directory.') raise FDroidException('Repository already exists.') if common.use_androguard(): pass elif 'aapt' not in test_config or not os.path.isfile(test_config['aapt']): # try to find a working aapt, in all the recent possible paths build_tools = os.path.join(test_config['sdk_path'], 'build-tools') aaptdirs = [] aaptdirs.append(os.path.join(build_tools, test_config['build_tools'])) aaptdirs.append(build_tools) for f in os.listdir(build_tools): if os.path.isdir(os.path.join(build_tools, f)): aaptdirs.append(os.path.join(build_tools, f)) for d in sorted(aaptdirs, reverse=True): if os.path.isfile(os.path.join(d, 'aapt')): aapt = os.path.join(d, 'aapt') break if aapt and os.path.isfile(aapt): dirname = os.path.basename(os.path.dirname(aapt)) if dirname == 'build-tools': # this is the old layout, before versioned build-tools test_config['build_tools'] = '' else: test_config['build_tools'] = dirname common.write_to_config(test_config, 'build_tools') common.ensure_build_tools_exists(test_config) # now that we have a local config.py, read configuration... config = common.read_config(options) # the NDK is optional and there may be multiple versions of it, so it's # left for the user to configure # find or generate the keystore for the repo signing key. First try the # path written in the default config.py. Then check if the user has # specified a path from the command line, which will trump all others. # Otherwise, create ~/.local/share/fdroidserver and stick it in there. If # keystore is set to NONE, that means that Java will look for keys in a # Hardware Security Module aka Smartcard. keystore = config['keystore'] if options.keystore: keystore = os.path.abspath(options.keystore) if options.keystore == 'NONE': keystore = options.keystore else: keystore = os.path.abspath(options.keystore) if not os.path.exists(keystore): logging.info('"' + keystore + '" does not exist, creating a new keystore there.') common.write_to_config(test_config, 'keystore', keystore) repo_keyalias = None keydname = None if options.repo_keyalias: repo_keyalias = options.repo_keyalias common.write_to_config(test_config, 'repo_keyalias', repo_keyalias) if options.distinguished_name: keydname = options.distinguished_name common.write_to_config(test_config, 'keydname', keydname) if keystore == 'NONE': # we're using a smartcard common.write_to_config(test_config, 'repo_keyalias', '1') # seems to be the default disable_in_config('keypass', 'never used with smartcard') common.write_to_config(test_config, 'smartcardoptions', ('-storetype PKCS11 -providerName SunPKCS11-OpenSC ' + '-providerClass sun.security.pkcs11.SunPKCS11 ' + '-providerArg opensc-fdroid.cfg')) # find opensc-pkcs11.so if not os.path.exists('opensc-fdroid.cfg'): if os.path.exists('/usr/lib/opensc-pkcs11.so'): opensc_so = '/usr/lib/opensc-pkcs11.so' elif os.path.exists('/usr/lib64/opensc-pkcs11.so'): opensc_so = '/usr/lib64/opensc-pkcs11.so' else: files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so') if len(files) > 0: opensc_so = files[0] else: opensc_so = '/usr/lib/opensc-pkcs11.so' logging.warn('No OpenSC PKCS#11 module found, ' + 'install OpenSC then edit "opensc-fdroid.cfg"!') with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f: opensc_fdroid = f.read() opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid, flags=re.MULTILINE) with open('opensc-fdroid.cfg', 'w') as f: f.write(opensc_fdroid) elif os.path.exists(keystore): to_set = ['keystorepass', 'keypass', 'repo_keyalias', 'keydname'] if repo_keyalias: to_set.remove('repo_keyalias') if keydname: to_set.remove('keydname') logging.warning('\n' + _('Using existing keystore "{path}"').format(path=keystore) + '\n' + _('Now set these in config.py:') + ' ' + ', '.join(to_set) + '\n') else: password = common.genpassword() c = dict(test_config) c['keystorepass'] = password c['keypass'] = password c['repo_keyalias'] = socket.getfqdn() c['keydname'] = 'CN=' + c['repo_keyalias'] + ', OU=F-Droid' common.write_to_config(test_config, 'keystorepass', password) common.write_to_config(test_config, 'keypass', password) common.write_to_config(test_config, 'repo_keyalias', c['repo_keyalias']) common.write_to_config(test_config, 'keydname', c['keydname']) common.genkeystore(c) msg = '\n' msg += _('Built repo based in "%s" with this config:') % fdroiddir msg += '\n\n Android SDK:\t\t\t' + config['sdk_path'] if aapt: msg += '\n Android SDK Build Tools:\t' + os.path.dirname(aapt) msg += '\n Android NDK r12b (optional):\t$ANDROID_NDK' msg += '\n ' + _('Keystore for signing key:\t') + keystore if repo_keyalias is not None: msg += '\n Alias for key in store:\t' + repo_keyalias msg += '\n\n' + '''To complete the setup, add your APKs to "%s" then run "fdroid update -c; fdroid update". You might also want to edit "config.py" to set the URL, repo name, and more. You should also set up a signing key (a temporary one might have been automatically generated). For more info: https://f-droid.org/docs/Setup_an_F-Droid_App_Repo and https://f-droid.org/docs/Signing_Process''' % os.path.join(fdroiddir, 'repo') logging.info(msg) fdroidserver-1.1.6/fdroidserver/install.py0000644000175000017500000001035713576156531020661 0ustar hanshans00000000000000#!/usr/bin/env python3 # # install.py - part of the FDroid server tools # Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # # 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 . import sys import os import glob from argparse import ArgumentParser import logging from . import _ from . import common from .common import SdkToolsPopen from .exception import FDroidException options = None config = None def devices(): p = SdkToolsPopen(['adb', "devices"]) if p.returncode != 0: raise FDroidException("An error occured when finding devices: %s" % p.output) lines = [l for l in p.output.splitlines() if not l.startswith('* ')] if len(lines) < 3: return [] lines = lines[1:-1] return [l.split()[0] for l in lines] def main(): global options, config # Parse command line... parser = ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]") common.setup_global_opts(parser) parser.add_argument("appid", nargs='*', help=_("applicationId with optional versionCode in the form APPID[:VERCODE]")) parser.add_argument("-a", "--all", action="store_true", default=False, help=_("Install all signed applications available")) options = parser.parse_args() if not options.appid and not options.all: parser.error(_("option %s: If you really want to install all the signed apps, use --all") % "all") config = common.read_config(options) output_dir = 'repo' if not os.path.isdir(output_dir): logging.info(_("No signed output directory - nothing to do")) sys.exit(0) if options.appid: vercodes = common.read_pkg_args(options.appid, True) apks = {appid: None for appid in vercodes} # Get the signed apk with the highest vercode for apkfile in sorted(glob.glob(os.path.join(output_dir, '*.apk'))): try: appid, vercode = common.publishednameinfo(apkfile) except FDroidException: continue if appid not in apks: continue if vercodes[appid] and vercode not in vercodes[appid]: continue apks[appid] = apkfile for appid, apk in apks.items(): if not apk: raise FDroidException(_("No signed apk available for %s") % appid) else: apks = {common.publishednameinfo(apkfile)[0]: apkfile for apkfile in sorted(glob.glob(os.path.join(output_dir, '*.apk')))} for appid, apk in apks.items(): # Get device list each time to avoid device not found errors devs = devices() if not devs: raise FDroidException(_("No attached devices found")) logging.info(_("Installing %s...") % apk) for dev in devs: logging.info(_("Installing '{apkfilename}' on {dev}...").format(apkfilename=apk, dev=dev)) p = SdkToolsPopen(['adb', "-s", dev, "install", apk]) fail = "" for line in p.output.splitlines(): if line.startswith("Failure"): fail = line[9:-1] if not fail: continue if fail == "INSTALL_FAILED_ALREADY_EXISTS": logging.warn(_("'{apkfilename}' is already installed on {dev}.") .format(apkfilename=apk, dev=dev)) else: raise FDroidException(_("Failed to install '{apkfilename}' on {dev}: {error}") .format(apkfilename=apk, dev=dev, error=fail)) logging.info('\n' + _('Finished')) if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/lint.py0000644000175000017500000006636113576156546020175 0ustar hanshans00000000000000#!/usr/bin/env python3 # # lint.py - part of the FDroid server tool # Copyright (C) 2013-2014 Daniel Martí # # 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 th # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public Licen # along with this program. If not, see . from argparse import ArgumentParser import glob import os import re import sys import urllib.parse from . import _ from . import common from . import metadata from . import rewritemeta config = None options = None def enforce_https(domain): return (re.compile(r'^[^h][^t][^t][^p][^s]://[^/]*' + re.escape(domain) + r'(/.*)?', re.IGNORECASE), domain + " URLs should always use https://") https_enforcings = [ enforce_https('github.com'), enforce_https('gitlab.com'), enforce_https('bitbucket.org'), enforce_https('apache.org'), enforce_https('google.com'), enforce_https('git.code.sf.net'), enforce_https('svn.code.sf.net'), enforce_https('anongit.kde.org'), enforce_https('savannah.nongnu.org'), enforce_https('git.savannah.nongnu.org'), enforce_https('download.savannah.nongnu.org'), enforce_https('savannah.gnu.org'), enforce_https('git.savannah.gnu.org'), enforce_https('download.savannah.gnu.org'), enforce_https('github.io'), enforce_https('gitlab.io'), enforce_https('githubusercontent.com'), ] def forbid_shortener(domain): return (re.compile(r'https?://[^/]*' + re.escape(domain) + r'/.*'), _("URL shorteners should not be used")) http_url_shorteners = [ forbid_shortener('1url.com'), forbid_shortener('adf.ly'), forbid_shortener('bc.vc'), forbid_shortener('bit.do'), forbid_shortener('bit.ly'), forbid_shortener('bitly.com'), forbid_shortener('budurl.com'), forbid_shortener('buzurl.com'), forbid_shortener('cli.gs'), forbid_shortener('cur.lv'), forbid_shortener('cutt.us'), forbid_shortener('db.tt'), forbid_shortener('filoops.info'), forbid_shortener('goo.gl'), forbid_shortener('is.gd'), forbid_shortener('ity.im'), forbid_shortener('j.mp'), forbid_shortener('l.gg'), forbid_shortener('lnkd.in'), forbid_shortener('moourl.com'), forbid_shortener('ow.ly'), forbid_shortener('para.pt'), forbid_shortener('po.st'), forbid_shortener('q.gs'), forbid_shortener('qr.ae'), forbid_shortener('qr.net'), forbid_shortener('rdlnk.com'), forbid_shortener('scrnch.me'), forbid_shortener('short.nr'), forbid_shortener('sn.im'), forbid_shortener('snipurl.com'), forbid_shortener('su.pr'), forbid_shortener('t.co'), forbid_shortener('tiny.cc'), forbid_shortener('tinyarrows.com'), forbid_shortener('tinyurl.com'), forbid_shortener('tr.im'), forbid_shortener('tweez.me'), forbid_shortener('twitthis.com'), forbid_shortener('twurl.nl'), forbid_shortener('tyn.ee'), forbid_shortener('u.bb'), forbid_shortener('u.to'), forbid_shortener('ur1.ca'), forbid_shortener('urlof.site'), forbid_shortener('v.gd'), forbid_shortener('vzturl.com'), forbid_shortener('x.co'), forbid_shortener('xrl.us'), forbid_shortener('yourls.org'), forbid_shortener('zip.net'), forbid_shortener('✩.ws'), forbid_shortener('➡.ws'), ] http_checks = https_enforcings + http_url_shorteners + [ (re.compile(r'.*github\.com/[^/]+/[^/]+\.git'), _("Appending .git is not necessary")), (re.compile(r'.*://[^/]*(github|gitlab|bitbucket|rawgit)[^/]*/([^/]+/){1,3}master'), _("Use /HEAD instead of /master to point at a file in the default branch")), ] regex_checks = { 'WebSite': http_checks, 'SourceCode': http_checks, 'Repo': https_enforcings, 'UpdateCheckMode': https_enforcings, 'IssueTracker': http_checks + [ (re.compile(r'.*github\.com/[^/]+/[^/]+/*$'), _("/issues is missing")), (re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'), _("/issues is missing")), ], 'Donate': http_checks + [ (re.compile(r'.*flattr\.com'), _("Flattr donation methods belong in the FlattrID flag")), (re.compile(r'.*liberapay\.com'), _("Liberapay donation methods belong in the LiberapayID flag")), ], 'Changelog': http_checks, 'Author Name': [ (re.compile(r'^\s'), _("Unnecessary leading space")), (re.compile(r'.*\s$'), _("Unnecessary trailing space")), ], 'Summary': [ (re.compile(r'.*\b(free software|open source)\b.*', re.IGNORECASE), _("No need to specify that the app is Free Software")), (re.compile(r'.*((your|for).*android|android.*(app|device|client|port|version))', re.IGNORECASE), _("No need to specify that the app is for Android")), (re.compile(r'.*[a-z0-9][.!?]( |$)'), _("Punctuation should be avoided")), (re.compile(r'^\s'), _("Unnecessary leading space")), (re.compile(r'.*\s$'), _("Unnecessary trailing space")), ], 'Description': https_enforcings + http_url_shorteners + [ (re.compile(r'\s*[*#][^ .]'), _("Invalid bulleted list")), (re.compile(r'https://f-droid.org/[a-z][a-z](_[A-Za-z]{2,4})?/'), _("Locale included in f-droid.org URL")), (re.compile(r'^\s'), _("Unnecessary leading space")), (re.compile(r'.*\s$'), _("Unnecessary trailing space")), (re.compile(r'.*<(applet|base|body|button|embed|form|head|html|iframe|img|input|link|object|picture|script|source|style|svg|video).*', re.IGNORECASE), _("Forbidden HTML tags")), (re.compile(r'''.*\s+src=["']javascript:.*'''), _("Javascript in HTML src attributes")), ], } locale_pattern = re.compile(r'^[a-z]{2,3}(-[A-Z][A-Z])?$') def check_regexes(app): for f, checks in regex_checks.items(): for m, r in checks: v = app.get(f) t = metadata.fieldtype(f) if t == metadata.TYPE_MULTILINE: for l in v.splitlines(): if m.match(l): yield "%s at line '%s': %s" % (f, l, r) else: if v is None: continue if m.match(v): yield "%s '%s': %s" % (f, v, r) def get_lastbuild(builds): lowest_vercode = -1 lastbuild = None for build in builds: if not build.disable: vercode = int(build.versionCode) if lowest_vercode == -1 or vercode < lowest_vercode: lowest_vercode = vercode if not lastbuild or int(build.versionCode) > int(lastbuild.versionCode): lastbuild = build return lastbuild def check_update_check_data_url(app): """UpdateCheckData must have a valid HTTPS URL to protect checkupdates runs """ if app.UpdateCheckData: urlcode, codeex, urlver, verex = app.UpdateCheckData.split('|') for url in (urlcode, urlver): if url != '.': parsed = urllib.parse.urlparse(url) if not parsed.scheme or not parsed.netloc: yield _('UpdateCheckData not a valid URL: {url}').format(url=url) if parsed.scheme != 'https': yield _('UpdateCheckData must use HTTPS URL: {url}').format(url=url) def check_vercode_operation(app): if app.VercodeOperation and not common.VERCODE_OPERATION_RE.match(app.VercodeOperation): yield _('Invalid VercodeOperation: {field}').format(field=app.VercodeOperation) def check_ucm_tags(app): lastbuild = get_lastbuild(app.builds) if (lastbuild is not None and lastbuild.commit and app.UpdateCheckMode == 'RepoManifest' and not lastbuild.commit.startswith('unknown') and lastbuild.versionCode == app.CurrentVersionCode and not lastbuild.forcevercode and any(s in lastbuild.commit for s in '.,_-/')): yield _("Last used commit '{commit}' looks like a tag, but Update Check Mode is '{ucm}'")\ .format(commit=lastbuild.commit, ucm=app.UpdateCheckMode) def check_char_limits(app): limits = config['char_limits'] if len(app.Summary) > limits['summary']: yield _("Summary of length {length} is over the {limit} char limit")\ .format(length=len(app.Summary), limit=limits['summary']) if len(app.Description) > limits['description']: yield _("Description of length {length} is over the {limit} char limit")\ .format(length=len(app.Description), limit=limits['description']) def check_old_links(app): usual_sites = [ 'github.com', 'gitlab.com', 'bitbucket.org', ] old_sites = [ 'gitorious.org', 'code.google.com', ] if any(s in app.Repo for s in usual_sites): for f in ['WebSite', 'SourceCode', 'IssueTracker', 'Changelog']: v = app.get(f) if any(s in v for s in old_sites): yield _("App is in '{repo}' but has a link to {url}")\ .format(repo=app.Repo, url=v) def check_useless_fields(app): if app.UpdateCheckName == app.id: yield _("Update Check Name is set to the known app id - it can be removed") filling_ucms = re.compile(r'^(Tags.*|RepoManifest.*)') def check_checkupdates_ran(app): if filling_ucms.match(app.UpdateCheckMode): if not app.AutoName and not app.CurrentVersion and app.CurrentVersionCode == '0': yield _("UCM is set but it looks like checkupdates hasn't been run yet") def check_empty_fields(app): if not app.Categories: yield _("Categories are not set") all_categories = set([ "Connectivity", "Development", "Games", "Graphics", "Internet", "Money", "Multimedia", "Navigation", "Phone & SMS", "Reading", "Science & Education", "Security", "Sports & Health", "System", "Theming", "Time", "Writing", ]) def check_categories(app): for categ in app.Categories: if categ not in all_categories: yield _("Categories '%s' is not valid" % categ) def check_duplicates(app): if app.Name and app.Name == app.AutoName: yield _("Name '%s' is just the auto name - remove it") % app.Name links_seen = set() for f in ['Source Code', 'Web Site', 'Issue Tracker', 'Changelog']: v = app.get(f) if not v: continue v = v.lower() if v in links_seen: yield _("Duplicate link in '{field}': {url}").format(field=f, url=v) else: links_seen.add(v) name = app.Name or app.AutoName if app.Summary and name: if app.Summary.lower() == name.lower(): yield _("Summary '%s' is just the app's name") % app.Summary if app.Summary and app.Description and len(app.Description) == 1: if app.Summary.lower() == app.Description[0].lower(): yield _("Description '%s' is just the app's summary") % app.Summary seenlines = set() for l in app.Description.splitlines(): if len(l) < 1: continue if l in seenlines: yield _("Description has a duplicate line") seenlines.add(l) desc_url = re.compile(r'(^|[^[])\[([^ ]+)( |\]|$)') def check_mediawiki_links(app): wholedesc = ' '.join(app.Description) for um in desc_url.finditer(wholedesc): url = um.group(1) for m, r in http_checks: if m.match(url): yield _("URL {url} in Description: {error}").format(url=url, error=r) def check_bulleted_lists(app): validchars = ['*', '#'] lchar = '' lcount = 0 for l in app.Description.splitlines(): if len(l) < 1: lcount = 0 continue if l[0] == lchar and l[1] == ' ': lcount += 1 if lcount > 2 and lchar not in validchars: yield _("Description has a list (%s) but it isn't bulleted (*) nor numbered (#)") % lchar break else: lchar = l[0] lcount = 1 def check_builds(app): supported_flags = set(metadata.build_flags) # needed for YAML and JSON for build in app.builds: if build.disable: if build.disable.startswith('Generated by import.py'): yield _("Build generated by `fdroid import` - remove disable line once ready") continue for s in ['master', 'origin', 'HEAD', 'default', 'trunk']: if build.commit and build.commit.startswith(s): yield _("Branch '{branch}' used as commit in build '{versionName}'")\ .format(branch=s, versionName=build.versionName) for srclib in build.srclibs: if '@' in srclib: ref = srclib.split('@')[1].split('/')[0] if ref.startswith(s): yield _("Branch '{branch}' used as commit in srclib '{srclib}'")\ .format(branch=s, srclib=srclib) else: yield _('srclibs missing name and/or @') + ' (srclibs: ' + srclib + ')' for key in build.keys(): if key not in supported_flags: yield _('%s is not an accepted build field') % key def check_files_dir(app): dir_path = os.path.join('metadata', app.id) if not os.path.isdir(dir_path): return files = set() for name in os.listdir(dir_path): path = os.path.join(dir_path, name) if not (os.path.isfile(path) or name == 'signatures' or locale_pattern.match(name)): yield _("Found non-file at %s") % path continue files.add(name) used = {'signatures', } for build in app.builds: for fname in build.patch: if fname not in files: yield _("Unknown file '{filename}' in build '{versionName}'")\ .format(filename=fname, versionName=build.versionName) else: used.add(fname) for name in files.difference(used): if locale_pattern.match(name): continue yield _("Unused file at %s") % os.path.join(dir_path, name) def check_format(app): if options.format and not rewritemeta.proper_format(app): yield _("Run rewritemeta to fix formatting") def check_license_tag(app): '''Ensure all license tags are in https://spdx.org/license-list''' if app.License.rstrip('+') not in SPDX: yield _('Invalid license tag "%s"! Use only tags from https://spdx.org/license-list') \ % (app.License) def check_extlib_dir(apps): dir_path = os.path.join('build', 'extlib') unused_extlib_files = set() for root, dirs, files in os.walk(dir_path): for name in files: unused_extlib_files.add(os.path.join(root, name)[len(dir_path) + 1:]) used = set() for app in apps: for build in app.builds: for path in build.extlibs: if path not in unused_extlib_files: yield _("{appid}: Unknown extlib {path} in build '{versionName}'")\ .format(appid=app.id, path=path, versionName=build.versionName) else: used.add(path) for path in unused_extlib_files.difference(used): if any(path.endswith(s) for s in [ '.gitignore', 'source.txt', 'origin.txt', 'md5.txt', 'LICENSE', 'LICENSE.txt', 'COPYING', 'COPYING.txt', 'NOTICE', 'NOTICE.txt', ]): continue yield _("Unused extlib at %s") % os.path.join(dir_path, path) def check_app_field_types(app): """Check the fields have valid data types""" for field in app.keys(): v = app.get(field) t = metadata.fieldtype(field) if v is None: continue elif field == 'builds': if not isinstance(v, list): yield(_("{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!") .format(appid=app.id, field=field, type='list', fieldtype=v.__class__.__name__)) elif t == metadata.TYPE_LIST and not isinstance(v, list): yield(_("{appid}: {field} must be a '{type}', but it is a '{fieldtype}!'") .format(appid=app.id, field=field, type='list', fieldtype=v.__class__.__name__)) elif t == metadata.TYPE_STRING and not type(v) in (str, bool, dict): yield(_("{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!") .format(appid=app.id, field=field, type='str', fieldtype=v.__class__.__name__)) def check_for_unsupported_metadata_files(basedir=""): """Checks whether any non-metadata files are in metadata/""" global config return_value = False formats = config['accepted_formats'] for f in glob.glob(basedir + 'metadata/*') + glob.glob(basedir + 'metadata/.*'): if os.path.isdir(f): exists = False for t in formats: exists = exists or os.path.exists(f + '.' + t) if not exists: print(_('"%s/" has no matching metadata file!') % f) return_value = True elif os.path.splitext(f)[1][1:] in formats: packageName = os.path.splitext(os.path.basename(f))[0] if not common.is_valid_package_name(packageName): print('"' + packageName + '" is an invalid package name!\n' + 'https://developer.android.com/studio/build/application-id') return_value = True else: print('"' + f.replace(basedir, '') + '" is not a supported file format: (' + ','.join(formats) + ')') return_value = True return return_value def check_current_version_code(app): """Check that the CurrentVersionCode is currently available""" archive_policy = app.get('ArchivePolicy') if archive_policy and archive_policy.split()[0] == "0": return cv = app.get('CurrentVersionCode') if cv is not None and int(cv) == 0: return builds = app.get('builds') active_builds = 0 min_versionCode = None if builds: for build in builds: vc = int(build['versionCode']) if min_versionCode is None or min_versionCode > vc: min_versionCode = vc if not build.get('disable'): active_builds += 1 if cv == build['versionCode']: break if active_builds == 0: return # all builds are disabled if cv is not None and int(cv) < min_versionCode: yield(_('CurrentVersionCode {cv} is less than oldest build entry {versionCode}') .format(cv=cv, versionCode=min_versionCode)) def main(): global config, options # Parse command line... parser = ArgumentParser(usage="%(prog)s [options] [APPID [APPID ...]]") common.setup_global_opts(parser) parser.add_argument("-f", "--format", action="store_true", default=False, help=_("Also warn about formatting issues, like rewritemeta -l")) parser.add_argument("appid", nargs='*', help=_("applicationId in the form APPID")) metadata.add_metadata_arguments(parser) options = parser.parse_args() metadata.warnings_action = options.W config = common.read_config(options) # Get all apps... allapps = metadata.read_metadata(xref=True) apps = common.read_app_args(options.appid, allapps, False) anywarns = check_for_unsupported_metadata_files() apps_check_funcs = [] if len(options.appid) == 0: # otherwise it finds tons of unused extlibs apps_check_funcs.append(check_extlib_dir) for check_func in apps_check_funcs: for warn in check_func(apps.values()): anywarns = True print(warn) for appid, app in apps.items(): if app.Disabled: continue app_check_funcs = [ check_app_field_types, check_regexes, check_update_check_data_url, check_vercode_operation, check_ucm_tags, check_char_limits, check_old_links, check_checkupdates_ran, check_useless_fields, check_empty_fields, check_categories, check_duplicates, check_mediawiki_links, check_bulleted_lists, check_builds, check_files_dir, check_format, check_license_tag, check_current_version_code, ] for check_func in app_check_funcs: for warn in check_func(app): anywarns = True print("%s: %s" % (appid, warn)) if anywarns: sys.exit(1) # A compiled, public domain list of official SPDX license tags from: # https://github.com/sindresorhus/spdx-license-list/blob/v4.0.0/spdx-simple.json # The deprecated license tags have been removed from the list, they are at the # bottom, starting after the last license tags that start with Z. # This is at the bottom, since its a long list of data SPDX = [ "PublicDomain", # an F-Droid addition, until we can enforce a better option "0BSD", "AAL", "Abstyles", "Adobe-2006", "Adobe-Glyph", "ADSL", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "Afmparse", "AGPL-1.0", "AGPL-3.0-only", "AGPL-3.0-or-later", "Aladdin", "AMDPLPA", "AML", "AMPAS", "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APAFML", "APL-1.0", "APSL-1.0", "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0-cl8", "Artistic-1.0-Perl", "Artistic-1.0", "Artistic-2.0", "Bahyph", "Barr", "Beerware", "BitTorrent-1.0", "BitTorrent-1.1", "Borceux", "BSD-1-Clause", "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", "BSD-2-Clause-Patent", "BSD-2-Clause", "BSD-3-Clause-Attribution", "BSD-3-Clause-Clear", "BSD-3-Clause-LBNL", "BSD-3-Clause-No-Nuclear-License-2014", "BSD-3-Clause-No-Nuclear-License", "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause", "BSD-4-Clause-UC", "BSD-4-Clause", "BSD-Protection", "BSD-Source-Code", "BSL-1.0", "bzip2-1.0.5", "bzip2-1.0.6", "Caldera", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5", "CC-BY-3.0", "CC-BY-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", "CC-BY-NC-3.0", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0", "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0", "CC-BY-ND-4.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.5", "CC-BY-SA-3.0", "CC-BY-SA-4.0", "CC0-1.0", "CDDL-1.0", "CDDL-1.1", "CDLA-Permissive-1.0", "CDLA-Sharing-1.0", "CECILL-1.0", "CECILL-1.1", "CECILL-2.0", "CECILL-2.1", "CECILL-B", "CECILL-C", "ClArtistic", "CNRI-Jython", "CNRI-Python-GPL-Compatible", "CNRI-Python", "Condor-1.1", "CPAL-1.0", "CPL-1.0", "CPOL-1.02", "Crossword", "CrystalStacker", "CUA-OPL-1.0", "Cube", "curl", "D-FSL-1.0", "diffmark", "DOC", "Dotseqn", "DSDP", "dvipdfm", "ECL-1.0", "ECL-2.0", "EFL-1.0", "EFL-2.0", "eGenix", "Entessa", "EPL-1.0", "EPL-2.0", "ErlPL-1.1", "EUDatagrid", "EUPL-1.0", "EUPL-1.1", "EUPL-1.2", "Eurosym", "Fair", "Frameworx-1.0", "FreeImage", "FSFAP", "FSFUL", "FSFULLR", "FTL", "GFDL-1.1-only", "GFDL-1.1-or-later", "GFDL-1.2-only", "GFDL-1.2-or-later", "GFDL-1.3-only", "GFDL-1.3-or-later", "Giftware", "GL2PS", "Glide", "Glulxe", "gnuplot", "GPL-1.0-only", "GPL-1.0-or-later", "GPL-2.0-only", "GPL-2.0-or-later", "GPL-3.0-only", "GPL-3.0-or-later", "gSOAP-1.3b", "HaskellReport", "HPND", "IBM-pibs", "ICU", "IJG", "ImageMagick", "iMatix", "Imlib2", "Info-ZIP", "Intel-ACPI", "Intel", "Interbase-1.0", "IPA", "IPL-1.0", "ISC", "JasPer-2.0", "JSON", "LAL-1.2", "LAL-1.3", "Latex2e", "Leptonica", "LGPL-2.0-only", "LGPL-2.0-or-later", "LGPL-2.1-only", "LGPL-2.1-or-later", "LGPL-3.0-only", "LGPL-3.0-or-later", "LGPLLR", "Libpng", "libtiff", "LiLiQ-P-1.1", "LiLiQ-R-1.1", "LiLiQ-Rplus-1.1", "LPL-1.0", "LPL-1.02", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3a", "LPPL-1.3c", "MakeIndex", "MirOS", "MIT-advertising", "MIT-CMU", "MIT-enna", "MIT-feh", "MIT", "MITNFA", "Motosoto", "mpich2", "MPL-1.0", "MPL-1.1", "MPL-2.0-no-copyleft-exception", "MPL-2.0", "MS-PL", "MS-RL", "MTLL", "Multics", "Mup", "NASA-1.3", "Naumen", "NBPL-1.0", "NCSA", "Net-SNMP", "NetCDF", "Newsletr", "NGPL", "NLOD-1.0", "NLPL", "Nokia", "NOSL", "Noweb", "NPL-1.0", "NPL-1.1", "NPOSL-3.0", "NRL", "NTP", "OCCT-PL", "OCLC-2.0", "ODbL-1.0", "OFL-1.0", "OFL-1.1", "OGTSL", "OLDAP-1.1", "OLDAP-1.2", "OLDAP-1.3", "OLDAP-1.4", "OLDAP-2.0.1", "OLDAP-2.0", "OLDAP-2.1", "OLDAP-2.2.1", "OLDAP-2.2.2", "OLDAP-2.2", "OLDAP-2.3", "OLDAP-2.4", "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OLDAP-2.8", "OML", "OpenSSL", "OPL-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1", "OSL-3.0", "PDDL-1.0", "PHP-3.0", "PHP-3.01", "Plexus", "PostgreSQL", "psfrag", "psutils", "Python-2.0", "Qhull", "QPL-1.0", "Rdisc", "RHeCos-1.1", "RPL-1.1", "RPL-1.5", "RPSL-1.0", "RSA-MD", "RSCPL", "Ruby", "SAX-PD", "Saxpath", "SCEA", "Sendmail", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", "SimPL-2.0", "SISSL-1.2", "SISSL", "Sleepycat", "SMLNJ", "SMPPL", "SNIA", "Spencer-86", "Spencer-94", "Spencer-99", "SPL-1.0", "SugarCRM-1.1.3", "SWL", "TCL", "TCP-wrappers", "TMate", "TORQUE-1.1", "TOSL", "Unicode-DFS-2015", "Unicode-DFS-2016", "Unicode-TOU", "Unlicense", "UPL-1.0", "Vim", "VOSTROM", "VSL-1.0", "W3C-19980720", "W3C-20150513", "W3C", "Watcom-1.0", "Wsuipa", "WTFPL", "X11", "Xerox", "XFree86-1.1", "xinetd", "Xnet", "xpp", "XSkat", "YPL-1.0", "YPL-1.1", "Zed", "Zend-2.0", "Zimbra-1.3", "Zimbra-1.4", "zlib-acknowledgement", "Zlib", "ZPL-1.1", "ZPL-2.0", "ZPL-2.1", ] if __name__ == "__main__": main() fdroidserver-1.1.6/fdroidserver/metadata.py0000644000175000017500000014467013576156546021007 0ustar hanshans00000000000000#!/usr/bin/env python3 # # metadata.py - part of the FDroid server tools # Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Martí # Copyright (C) 2017-2018 Michael Pöhn # # 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 . import json import os import re import glob import html import logging import textwrap import io import yaml from collections import OrderedDict import fdroidserver.common from fdroidserver import _ from fdroidserver.exception import MetaDataException, FDroidException srclibs = None warnings_action = None def warn_or_exception(value): '''output warning or Exception depending on -W''' if warnings_action == 'ignore': pass elif warnings_action == 'error': raise MetaDataException(value) else: logging.warning(value) # To filter which ones should be written to the metadata files if # present app_fields = set([ 'Disabled', 'AntiFeatures', 'Provides', 'Categories', 'License', 'Author Name', 'Author Email', 'Author Web Site', 'Web Site', 'Source Code', 'Issue Tracker', 'Translation', 'Changelog', 'Donate', 'FlattrID', 'LiberapayID', 'Bitcoin', 'Litecoin', 'Name', 'Auto Name', 'Summary', 'Description', 'Requires Root', 'Repo Type', 'Repo', 'Binaries', 'Maintainer Notes', 'Archive Policy', 'Auto Update Mode', 'Update Check Mode', 'Update Check Ignore', 'Vercode Operation', 'Update Check Name', 'Update Check Data', 'Current Version', 'Current Version Code', 'No Source Since', 'Build', 'comments', # For formats that don't do inline comments 'builds', # For formats that do builds as a list ]) yaml_app_field_order = [ 'Disabled', 'AntiFeatures', 'Provides', 'Categories', 'License', 'AuthorName', 'AuthorEmail', 'AuthorWebSite', 'WebSite', 'SourceCode', 'IssueTracker', 'Translation', 'Changelog', 'Donate', 'FlattrID', 'LiberapayID', 'Bitcoin', 'Litecoin', '\n', 'Name', 'AutoName', 'Summary', 'Description', '\n', 'RequiresRoot', '\n', 'RepoType', 'Repo', 'Binaries', '\n', 'Builds', '\n', 'MaintainerNotes', '\n', 'ArchivePolicy', 'AutoUpdateMode', 'UpdateCheckMode', 'UpdateCheckIgnore', 'VercodeOperation', 'UpdateCheckName', 'UpdateCheckData', 'CurrentVersion', 'CurrentVersionCode', '\n', 'NoSourceSince', ] yaml_app_fields = [x for x in yaml_app_field_order if x != '\n'] class App(dict): def __init__(self, copydict=None): if copydict: super().__init__(copydict) return super().__init__() self.Disabled = None self.AntiFeatures = [] self.Provides = None self.Categories = [] self.License = 'Unknown' self.AuthorName = None self.AuthorEmail = None self.AuthorWebSite = None self.WebSite = '' self.SourceCode = '' self.IssueTracker = '' self.Translation = '' self.Changelog = '' self.Donate = None self.FlattrID = None self.LiberapayID = None self.Bitcoin = None self.Litecoin = None self.Name = None self.AutoName = '' self.Summary = '' self.Description = '' self.RequiresRoot = False self.RepoType = '' self.Repo = '' self.Binaries = None self.MaintainerNotes = '' self.ArchivePolicy = None self.AutoUpdateMode = 'None' self.UpdateCheckMode = 'None' self.UpdateCheckIgnore = None self.VercodeOperation = None self.UpdateCheckName = None self.UpdateCheckData = None self.CurrentVersion = '' self.CurrentVersionCode = None self.NoSourceSince = '' self.id = None self.metadatapath = None self.builds = [] self.comments = {} self.added = None self.lastUpdated = None def __getattr__(self, name): if name in self: return self[name] else: raise AttributeError("No such attribute: " + name) def __setattr__(self, name, value): self[name] = value def __delattr__(self, name): if name in self: del self[name] else: raise AttributeError("No such attribute: " + name) def get_last_build(self): if len(self.builds) > 0: return self.builds[-1] else: return Build() TYPE_UNKNOWN = 0 TYPE_OBSOLETE = 1 TYPE_STRING = 2 TYPE_BOOL = 3 TYPE_LIST = 4 TYPE_SCRIPT = 5 TYPE_MULTILINE = 6 TYPE_BUILD = 7 TYPE_INT = 8 fieldtypes = { 'Description': TYPE_MULTILINE, 'MaintainerNotes': TYPE_MULTILINE, 'Categories': TYPE_LIST, 'AntiFeatures': TYPE_LIST, 'Build': TYPE_BUILD, 'BuildVersion': TYPE_OBSOLETE, 'UseBuilt': TYPE_OBSOLETE, } def fieldtype(name): name = name.replace(' ', '') if name in fieldtypes: return fieldtypes[name] return TYPE_STRING # In the order in which they are laid out on files build_flags_order = [ 'disable', 'commit', 'timeout', 'subdir', 'submodules', 'sudo', 'init', 'patch', 'gradle', 'maven', 'buildozer', 'output', 'srclibs', 'oldsdkloc', 'encoding', 'forceversion', 'forcevercode', 'rm', 'extlibs', 'prebuild', 'androidupdate', 'target', 'scanignore', 'scandelete', 'build', 'buildjni', 'ndk', 'preassemble', 'gradleprops', 'antcommands', 'novcheck', 'antifeatures', ] # old .txt format has version name/code inline in the 'Build:' line # but YAML and JSON have a explicit key for them build_flags = ['versionName', 'versionCode'] + build_flags_order class Build(dict): def __init__(self, copydict=None): super().__init__() self.disable = '' self.commit = None self.timeout = None self.subdir = None self.submodules = False self.sudo = '' self.init = '' self.patch = [] self.gradle = [] self.maven = False self.buildozer = False self.output = None self.srclibs = [] self.oldsdkloc = False self.encoding = None self.forceversion = False self.forcevercode = False self.rm = [] self.extlibs = [] self.prebuild = '' self.androidupdate = [] self.target = None self.scanignore = [] self.scandelete = [] self.build = '' self.buildjni = [] self.ndk = None self.preassemble = [] self.gradleprops = [] self.antcommands = [] self.novcheck = False self.antifeatures = [] if copydict: super().__init__(copydict) return def __getattr__(self, name): if name in self: return self[name] else: raise AttributeError("No such attribute: " + name) def __setattr__(self, name, value): self[name] = value def __delattr__(self, name): if name in self: del self[name] else: raise AttributeError("No such attribute: " + name) def build_method(self): for f in ['maven', 'gradle', 'buildozer']: if self.get(f): return f if self.output: return 'raw' return 'ant' # like build_method, but prioritize output= def output_method(self): if self.output: return 'raw' for f in ['maven', 'gradle', 'buildozer']: if self.get(f): return f return 'ant' def ndk_path(self): version = self.ndk if not version: version = 'r12b' # falls back to latest paths = fdroidserver.common.config['ndk_paths'] if version not in paths: return '' return paths[version] flagtypes = { 'versionCode': TYPE_INT, 'extlibs': TYPE_LIST, 'srclibs': TYPE_LIST, 'patch': TYPE_LIST, 'rm': TYPE_LIST, 'buildjni': TYPE_LIST, 'preassemble': TYPE_LIST, 'androidupdate': TYPE_LIST, 'scanignore': TYPE_LIST, 'scandelete': TYPE_LIST, 'gradle': TYPE_LIST, 'antcommands': TYPE_LIST, 'gradleprops': TYPE_LIST, 'sudo': TYPE_SCRIPT, 'init': TYPE_SCRIPT, 'prebuild': TYPE_SCRIPT, 'build': TYPE_SCRIPT, 'submodules': TYPE_BOOL, 'oldsdkloc': TYPE_BOOL, 'forceversion': TYPE_BOOL, 'forcevercode': TYPE_BOOL, 'novcheck': TYPE_BOOL, 'antifeatures': TYPE_LIST, 'timeout': TYPE_INT, } def flagtype(name): if name in flagtypes: return flagtypes[name] return TYPE_STRING class FieldValidator(): """ Designates App metadata field types and checks that it matches 'name' - The long name of the field type 'matching' - List of possible values or regex expression 'sep' - Separator to use if value may be a list 'fields' - Metadata fields (Field:Value) of this type """ def __init__(self, name, matching, fields): self.name = name self.matching = matching self.compiled = re.compile(matching) self.fields = fields def check(self, v, appid): if not v: return if type(v) == list: values = v else: values = [v] for v in values: if not self.compiled.match(v): warn_or_exception(_("'{value}' is not a valid {field} in {appid}. Regex pattern: {pattern}") .format(value=v, field=self.name, appid=appid, pattern=self.matching)) # Generic value types valuetypes = { FieldValidator("Flattr ID", r'^[0-9a-z]+$', ['FlattrID']), FieldValidator("Liberapay ID", r'^[0-9]+$', ['LiberapayID']), FieldValidator("HTTP link", r'^http[s]?://', ["WebSite", "SourceCode", "IssueTracker", "Translation", "Changelog", "Donate"]), FieldValidator("Email", r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', ["AuthorEmail"]), FieldValidator("Bitcoin address", r'^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$', ["Bitcoin"]), FieldValidator("Litecoin address", r'^L[a-zA-Z0-9]{33}$', ["Litecoin"]), FieldValidator("Repo Type", r'^(git|git-svn|svn|hg|bzr|srclib)$', ["RepoType"]), FieldValidator("Binaries", r'^http[s]?://', ["Binaries"]), FieldValidator("Archive Policy", r'^[0-9]+ versions$', ["ArchivePolicy"]), FieldValidator("Anti-Feature", r'^(Ads|Tracking|NonFreeNet|NonFreeDep|NonFreeAdd|UpstreamNonFree|NonFreeAssets|KnownVuln|ApplicationDebuggable|NoSourceSince)$', ["AntiFeatures"]), FieldValidator("Auto Update Mode", r"^(Version .+|None)$", ["AutoUpdateMode"]), FieldValidator("Update Check Mode", r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", ["UpdateCheckMode"]) } # Check an app's metadata information for integrity errors def check_metadata(app): for v in valuetypes: for k in v.fields: v.check(app[k], app.id) # Formatter for descriptions. Create an instance, and call parseline() with # each line of the description source from the metadata. At the end, call # end() and then text_txt and text_html will contain the result. class DescriptionFormatter: stNONE = 0 stPARA = 1 stUL = 2 stOL = 3 def __init__(self, linkres): self.bold = False self.ital = False self.state = self.stNONE self.laststate = self.stNONE self.text_html = '' self.text_txt = '' self.html = io.StringIO() self.text = io.StringIO() self.para_lines = [] self.linkResolver = None self.linkResolver = linkres def endcur(self, notstates=None): if notstates and self.state in notstates: return if self.state == self.stPARA: self.endpara() elif self.state == self.stUL: self.endul() elif self.state == self.stOL: self.endol() def endpara(self): self.laststate = self.state self.state = self.stNONE whole_para = ' '.join(self.para_lines) self.addtext(whole_para) wrapped = textwrap.fill(whole_para, 80, break_long_words=False, break_on_hyphens=False) self.text.write(wrapped) self.html.write('

') del self.para_lines[:] def endul(self): self.html.write('') self.laststate = self.state self.state = self.stNONE def endol(self): self.html.write('') self.laststate = self.state self.state = self.stNONE def formatted(self, txt, htmlbody): res = '' if htmlbody: txt = html.escape(txt, quote=False) while True: index = txt.find("''") if index == -1: return res + txt res += txt[:index] txt = txt[index:] if txt.startswith("'''"): if htmlbody: if self.bold: res += '' else: res += '' self.bold = not self.bold txt = txt[3:] else: if htmlbody: if self.ital: res += '' else: res += '' self.ital = not self.ital txt = txt[2:] def linkify(self, txt): res_plain = '' res_html = '' while True: index = txt.find("[") if index == -1: return (res_plain + self.formatted(txt, False), res_html + self.formatted(txt, True)) res_plain += self.formatted(txt[:index], False) res_html += self.formatted(txt[:index], True) txt = txt[index:] if txt.startswith("[["): index = txt.find("]]") if index == -1: warn_or_exception(_("Unterminated ]]")) url = txt[2:index] if self.linkResolver: url, urltext = self.linkResolver(url) else: urltext = url res_html += '' + html.escape(urltext, quote=False) + '' res_plain += urltext txt = txt[index + 2:] else: index = txt.find("]") if index == -1: warn_or_exception(_("Unterminated ]")) url = txt[1:index] index2 = url.find(' ') if index2 == -1: urltxt = url else: urltxt = url[index2 + 1:] url = url[:index2] if url == urltxt: warn_or_exception(_("URL title is just the URL, use brackets: [URL]")) res_html += '' + html.escape(urltxt, quote=False) + '' res_plain += urltxt if urltxt != url: res_plain += ' (' + url + ')' txt = txt[index + 1:] def addtext(self, txt): p, h = self.linkify(txt) self.html.write(h) def parseline(self, line): if not line: self.endcur() elif line.startswith('* '): self.endcur([self.stUL]) if self.state != self.stUL: self.html.write('