diff -Nru fdroidserver-0.9.1/buildserver/gradle fdroidserver-1.0.0/buildserver/gradle --- fdroidserver-0.9.1/buildserver/gradle 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/buildserver/gradle 2017-12-18 08:55:53.000000000 +0000 @@ -26,7 +26,7 @@ d_plugin_v=(4.1 3.3 2.14.1 2.14.1 2.12 2.12 2.4 2.4 2.3 2.2.1 2.2.1 2.1 2.1 1.12 1.12 1.12 1.11 1.10 1.9 1.8 1.6 1.6 1.4 1.4) # All gradle versions we know about -plugin_v=(4.3.1 4.3 4.2.1 4.2 4.1 4.0.2 4.0.1 4.0 3.5.1 3.5 3.4.1 3.4 3.3 3.2.1 3.2 3.1 3.0 2.14.1 2.14 2.13 2.12 2.11 2.10 2.9 2.8 2.7 2.6 2.5 2.4 2.3 2.2.1 2.2 2.1 1.12 1.11 1.10 1.9 1.8 1.7 1.6 1.4) +plugin_v=(4.4 4.3.1 4.3 4.2.1 4.2 4.1 4.0.2 4.0.1 4.0 3.5.1 3.5 3.4.1 3.4 3.3 3.2.1 3.2 3.1 3.0 2.14.1 2.14 2.13 2.12 2.11 2.10 2.9 2.8 2.7 2.6 2.5 2.4 2.3 2.2.1 2.2 2.1 1.12 1.11 1.10 1.9 1.8 1.7 1.6 1.4) v_all=${plugin_v[@]} echo "Available gradle versions: ${v_all[@]}" diff -Nru fdroidserver-0.9.1/buildserver/provision-android-sdk fdroidserver-1.0.0/buildserver/provision-android-sdk --- fdroidserver-0.9.1/buildserver/provision-android-sdk 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/buildserver/provision-android-sdk 2017-12-04 21:23:24.000000000 +0000 @@ -74,12 +74,25 @@ EOH mkdir -p $ANDROID_HOME/licenses/ + cat << EOF > $ANDROID_HOME/licenses/android-sdk-license 8933bad161af4178b1185d1a37fbf41ea5269c55 + d56f5187479451eabf01fb78af6dfcb131a6481e EOF -echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > $ANDROID_HOME/licenses/android-sdk-preview-license + +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" diff -Nru fdroidserver-0.9.1/completion/bash-completion fdroidserver-1.0.0/completion/bash-completion --- fdroidserver-0.9.1/completion/bash-completion 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/completion/bash-completion 2017-12-14 15:54:01.000000000 +0000 @@ -35,7 +35,7 @@ } __package() { - files="$(__by_ext txt) $(__by_ext yaml) $(__by_ext json) $(__by_ext xml)" + files="$(__by_ext txt) $(__by_ext yml) $(__by_ext json)" COMPREPLY=( $( compgen -W "$files" -- $cur ) ) } @@ -264,6 +264,12 @@ __complete_options } +__complete_mirror() { + opts="-v" + lopts="--archive --output-dir" + __complete_options +} + __complete_nightly() { opts="-v -q" lopts="--show-secret-var" @@ -316,6 +322,7 @@ init \ install \ lint \ +mirror \ nightly \ publish \ readmeta \ @@ -345,11 +352,6 @@ } } -_fd-commit() { - __package -} - complete -F _fdroid fdroid -complete -F _fd-commit fd-commit return 0 diff -Nru fdroidserver-0.9.1/debian/changelog fdroidserver-1.0.0/debian/changelog --- fdroidserver-0.9.1/debian/changelog 2017-11-27 19:39:20.000000000 +0000 +++ fdroidserver-1.0.0/debian/changelog 2018-01-03 20:55:54.000000000 +0000 @@ -1,8 +1,14 @@ -fdroidserver (0.9.1-1~zesty1) zesty; urgency=medium +fdroidserver (1.0.0-1~zesty) zesty; urgency=medium * backport to zesty - -- Hans-Christoph Steiner Mon, 27 Nov 2017 20:39:20 +0100 + -- Hans-Christoph Steiner Wed, 03 Jan 2018 21:55:54 +0100 + +fdroidserver (1.0.0-1) unstable; urgency=medium + + * New upstream version 1.0.0 + + -- Hans-Christoph Steiner Wed, 03 Jan 2018 21:27:18 +0100 fdroidserver (0.9.1-1) unstable; urgency=medium diff -Nru fdroidserver-0.9.1/debian/control fdroidserver-1.0.0/debian/control --- fdroidserver-0.9.1/debian/control 2017-11-27 19:34:10.000000000 +0000 +++ fdroidserver-1.0.0/debian/control 2018-01-03 20:46:48.000000000 +0000 @@ -7,8 +7,9 @@ bash-completion, dh-python, python3, + python3-babel, python3-setuptools, -Standards-Version: 4.1.1 +Standards-Version: 4.1.3 X-Python3-Version: >= 3.4 Testsuite: autopkgtest-pkg-python Homepage: https://f-droid.org @@ -19,7 +20,6 @@ Architecture: all Depends: ${python3:Depends}, default-jdk-headless | default-jdk | openjdk-7-jdk | oracle-java7-jdk | oracle-java8-jdk | oracle-java9-jdk | oracle-java7-installer | oracle-java8-installer | oracle-java9-installer, - python3-qrcode, python3-ruamel.yaml, rsync, ${misc:Depends} @@ -28,6 +28,7 @@ apksigner, git, opensc, + openssh-client, s3cmd, zipalign Suggests: gradle, diff -Nru fdroidserver-0.9.1/examples/config.py fdroidserver-1.0.0/examples/config.py --- fdroidserver-0.9.1/examples/config.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/examples/config.py 2017-12-23 00:01:43.000000000 +0000 @@ -216,11 +216,12 @@ # 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 is based on apache-libcloud, which supports basically all cloud -# storage services, so it should be easy to port the fdroid server tools to -# any of them. +# 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' diff -Nru fdroidserver-0.9.1/fd-commit fdroidserver-1.0.0/fd-commit --- fdroidserver-0.9.1/fd-commit 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fd-commit 1970-01-01 00:00:00.000000000 +0000 @@ -1,118 +0,0 @@ -#!/bin/bash -# -# fd-commit - part of the F-Droid server tools -# Commits updates to apps, allowing you to edit the commit messages -# -# 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 . - -commands=() - -if [ ! -d metadata ]; then - if [ -d ../metadata ]; then - cd .. - else - echo "No metadata files found!" - exit 2 - fi -fi - -while read line; do - - case "$line" in - *'??'*'metadata/'*'.txt') new=true ;; - *'M'*'metadata/'*'.txt') new=false ;; - *) continue ;; - esac - - file=${line##* } - id=${file##*/} - id=${id%.txt*} - - if [ $# -gt 0 ]; then - case "$@" in - *" $id "*) ;; # Middle - "$id "*) ;; # Start - *" $id") ;; # End - "$id") ;; # Alone - *) continue ;; # Missing - esac - fi - - [ -d metadata/$id ] && extra=metadata/$id || extra= - - name= autoname= - while read l; do - case "$l" in - 'Auto Name:'*) autoname=${l#*:} ;; - 'Name:'*) name=${l#*:} ;; - 'Summary:'*) break ;; - esac - done < "$file" - - if [ -n "$name" ]; then - fullname="$name" - elif [ -n "$autoname" ]; then - fullname="$autoname" - else - fullname="$id" - fi - - if $new; then - message="New app: $fullname" - else - onlybuild=true - newbuild=false - disable=false - while read line; do - case "$line" in - '-Build:'*) onlybuild=false ;; - '+Build:'*) - $newbuild && onlybuild=false - newbuild=true - build=${line#*:} - version=${build%%,*} - build=${build#*,} - vercode=${build%%,*} - ;; - '+'*'disable='*) - $newbuild && $onlybuild && disable=true - ;; - esac - done < <(git diff HEAD -- "$file") - - if $newbuild && $onlybuild; then - if $disable; then - message="Don't update $fullname to $version ($vercode)" - else - message="Update $fullname to $version ($vercode)" - fi - else - message="$fullname:" - fi - fi - - message=${message//\"/\\\"} - commands+=("git add -- $file $extra && git commit -m \"$message\" -e -v") - -done < <(git status --porcelain metadata) - -[ -z "$commands" ] && exit 0 - -git reset >/dev/null -for cmd in "${commands[@]}"; do - eval "$cmd" - git reset >/dev/null -done diff -Nru fdroidserver-0.9.1/fdroid fdroidserver-1.0.0/fdroid --- fdroidserver-0.9.1/fdroid 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroid 2017-12-07 16:02:32.000000000 +0000 @@ -48,11 +48,12 @@ ("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(_("usage: ") + _("fdroid [] [-h|--help|--version|]")) print("") print(_("Valid commands are:")) for cmd, summary in commands.items(): diff -Nru fdroidserver-0.9.1/fdroidserver/build.py fdroidserver-1.0.0/fdroidserver/build.py --- fdroidserver-0.9.1/fdroidserver/build.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/build.py 2018-01-03 14:43:42.000000000 +0000 @@ -23,6 +23,7 @@ import glob import subprocess import re +import resource import tarfile import traceback import time @@ -78,6 +79,7 @@ buildserverid = subprocess.check_output(['vagrant', 'ssh', '-c', 'cat /home/vagrant/buildserverid'], cwd='builder').rstrip() + logging.debug(_('Fetched buildserverid from VM: ') + buildserverid) # Open SSH connection... logging.info("Connecting to virtual machine...") @@ -99,18 +101,21 @@ # Helper to copy the contents of a directory to the server... def send_dir(path): logging.debug("rsyncing " + path + " to " + ftp.getcwd()) - subprocess.check_call(['rsync', '-rple', - '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()]) + # 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__)) @@ -284,6 +289,10 @@ path) +def _get_build_timestamp(): + return time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + + def transform_first_char(string, method): """Uses method() on the first character of string.""" if len(string) == 0: @@ -410,6 +419,16 @@ 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: @@ -948,7 +967,7 @@ if server: # When using server mode, still keep a local cache of the repo, by # grabbing the source now. - vcs.gotorevision(build.commit) + vcs.gotorevision(build.commit, refresh) build_server(app, build, vcs, build_dir, output_dir, log_dir, force) else: @@ -1041,6 +1060,7 @@ options = None config = None buildserverid = None +starttime = _get_build_timestamp() def main(): @@ -1110,7 +1130,7 @@ # Read all app and srclib metadata pkgs = common.read_pkg_args(options.appid, True) - allapps = metadata.read_metadata(not options.onserver, pkgs) + 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()): @@ -1120,6 +1140,19 @@ 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): @@ -1137,12 +1170,17 @@ # Build applications... failed_apps = {} build_succeeded = [] + max_apps_per_run = 10 for appid, app in apps.items(): + max_apps_per_run -= 1 + if max_apps_per_run < 1: + break first = True for build in app.builds: wikilog = None + build_starttime = _get_build_timestamp() tools_version_log = '' if not options.onserver: tools_version_log = get_android_tools_version_log(build.ndk_path()) @@ -1153,9 +1191,9 @@ # there are any. if first: vcs, build_dir = common.setup_vcs(app) - logging.info("Using %s" % vcs.clientversion()) 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, @@ -1239,7 +1277,7 @@ f.write('versionCode: %s\nversionName: %s\ncommit: %s\n' % (build.versionCode, build.versionName, build.commit)) f.write('Build completed at ' - + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + '\n') + + _get_build_timestamp() + '\n') f.write('\n' + tools_version_log + '\n') f.write(str(e)) logging.error("Could not build app %s: %s" % (appid, e)) @@ -1264,10 +1302,12 @@ newpage = site.Pages[lastbuildpage] with open(os.path.join('tmp', 'fdroidserverid')) as fp: fdroidserverid = fp.read().rstrip() - txt = "* build completed at " + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + '\n' \ + txt = "* build session started at " + starttime + '\n' \ + + "* this build started at " + build_starttime + '\n' \ + + "* this build completed at " + _get_build_timestamp() + '\n' \ + '* fdroidserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \ + fdroidserverid + ' ' + fdroidserverid + ']\n\n' - if options.onserver: + if buildserverid: txt += '* buildserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \ + buildserverid + ' ' + buildserverid + ']\n\n' txt += tools_version_log + '\n\n' @@ -1331,7 +1371,10 @@ logging.info(ngettext("{} build failed", "{} builds failed", len(failed_apps)).format(len(failed_apps))) - sys.exit(0) + # hack to ensure this exits, even is some threads are still running + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) if __name__ == "__main__": diff -Nru fdroidserver-0.9.1/fdroidserver/checkupdates.py fdroidserver-1.0.0/fdroidserver/checkupdates.py --- fdroidserver-0.9.1/fdroidserver/checkupdates.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/checkupdates.py 2017-12-06 21:46:17.000000000 +0000 @@ -33,7 +33,7 @@ from . import _ from . import common from . import metadata -from .exception import VCSException, FDroidException, MetaDataException +from .exception import VCSException, NoSubmodulesException, FDroidException, MetaDataException # Check for a new version by looking at a document retrieved via HTTP. @@ -110,8 +110,7 @@ last_build = app.get_last_build() - if last_build.submodules: - vcs.initsubmodules() + try_init_submodules(app, last_build, vcs) hpak = None htag = None @@ -207,8 +206,7 @@ if len(app.builds) > 0: last_build = app.builds[-1] - if last_build.submodules: - vcs.initsubmodules() + try_init_submodules(app, last_build, vcs) hpak = None hver = None @@ -300,6 +298,19 @@ 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): diff -Nru fdroidserver-0.9.1/fdroidserver/common.py fdroidserver-1.0.0/fdroidserver/common.py --- fdroidserver-0.9.1/fdroidserver/common.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/common.py 2017-12-28 22:07:26.000000000 +0000 @@ -40,7 +40,7 @@ import xml.etree.ElementTree as XMLElementTree from binascii import hexlify -from datetime import datetime +from datetime import datetime, timedelta from distutils.version import LooseVersion from queue import Queue from zipfile import ZipFile @@ -53,9 +53,13 @@ import fdroidserver.metadata from fdroidserver import _ -from fdroidserver.exception import FDroidException, VCSException, BuildException, VerificationException +from fdroidserver.exception import FDroidException, VCSException, NoSubmodulesException,\ + BuildException, VerificationException from .asynchronousfilereader import AsynchronousFileReader +# 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' # A signature block file with a .DSA, .RSA, or .EC extension CERT_PATH_REGEX = re.compile(r'^META-INF/.*\.(DSA|EC|RSA)$') @@ -83,7 +87,7 @@ 'r16': None, }, 'qt_sdk_path': None, - 'build_tools': "25.0.2", + 'build_tools': MINIMUM_AAPT_VERSION, 'force_build_tools': False, 'java_paths': None, 'ant': "ant", @@ -127,6 +131,13 @@ 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, @@ -387,9 +398,15 @@ minor = m.group(2) bugfix = m.group(3) # the Debian package has the version string like "v0.2-23.0.2" - if '.' not in bugfix and LooseVersion('.'.join((major, minor, bugfix))) < LooseVersion('0.2.2166767'): - logging.warning(_("'{aapt}' is too old, fdroid requires build-tools-23.0.0 or newer!") - .format(aapt=aapt)) + 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) @@ -444,17 +461,16 @@ return glob.glob('.fdroid.[a-jl-z]*[a-rt-z]') -def read_pkg_args(args, allow_vercodes=False): +def read_pkg_args(appid_versionCode_pairs, allow_vercodes=False): """ - :param args: arguments in the form of multiple appid:[vc] strings + :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 args: + if not appid_versionCode_pairs: return vercodes - for p in args: + for p in appid_versionCode_pairs: if allow_vercodes and ':' in p: package, vercode = p.split(':') else: @@ -468,13 +484,17 @@ return vercodes -def read_app_args(args, allapps, allow_vercodes=False): - """ - On top of what read_pkg_args does, this returns the whole app metadata, but - limiting the builds list to the builds matching the vercodes specified. +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(args, allow_vercodes) + vercodes = read_pkg_args(appid_versionCode_pairs, allow_vercodes) if not vercodes: return allapps @@ -783,7 +803,7 @@ def clientversioncmd(self): return ['git', '--version'] - def GitFetchFDroidPopen(self, gitargs, envs=dict(), cwd=None, output=True): + 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, @@ -792,10 +812,14 @@ enough. So we just throw the kitchen sink at it to see what sticks. + Also, because of CVE-2017-1000117, block all SSH URLs. ''' - if cwd is None: - cwd = self.local - git_config = [] + # + # supported in git >= 2.3 + git_config = [ + '-c', 'core.sshCommand=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 + ':') @@ -803,15 +827,11 @@ 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) - # add helpful tricks supported in git >= 2.3 - ssh_command = 'ssh -oBatchMode=yes -oStrictHostKeyChecking=yes' - git_config.append('-c') - git_config.append('core.sshCommand="' + ssh_command + '"') # git >= 2.10 envs.update({ 'GIT_TERMINAL_PROMPT': '0', - 'GIT_SSH_COMMAND': ssh_command, # git >= 2.3 + 'GIT_SSH': 'false', # for git < 2.3 }) - return FDroidPopen(['git', ] + git_config + gitargs, + return FDroidPopen(['git', ] + git_config + args, envs=envs, cwd=cwd, output=output) def checkrepo(self): @@ -830,7 +850,7 @@ def gotorevisionx(self, rev): if not os.path.exists(self.local): # Brand new checkout - p = FDroidPopen(['git', 'clone', self.remote, self.local], cwd=None) + p = self.git(['clone', self.remote, self.local]) if p.returncode != 0: self.clone_failed = True raise VCSException("Git clone failed", p.output) @@ -850,10 +870,10 @@ raise VCSException(_("Git clean failed"), p.output) if not self.refreshed: # Get latest commits and tags from remote - p = self.GitFetchFDroidPopen(['fetch', 'origin']) + p = self.git(['fetch', 'origin'], cwd=self.local) if p.returncode != 0: raise VCSException(_("Git fetch failed"), p.output) - p = self.GitFetchFDroidPopen(['fetch', '--prune', '--tags', 'origin'], output=False) + 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 @@ -882,7 +902,7 @@ self.checkrepo() submfile = os.path.join(self.local, '.gitmodules') if not os.path.isfile(submfile): - raise VCSException(_("No git submodules available")) + raise NoSubmodulesException(_("No git submodules available")) # fix submodules not accessible without an account and public key auth with open(submfile, 'r') as f: @@ -896,7 +916,7 @@ p = FDroidPopen(['git', 'submodule', 'sync'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git submodule sync failed"), p.output) - p = self.GitFetchFDroidPopen(['submodule', 'update', '--init', '--force', '--recursive']) + p = self.git(['submodule', 'update', '--init', '--force', '--recursive'], cwd=self.local) if p.returncode != 0: raise VCSException(_("Git submodule update failed"), p.output) @@ -939,10 +959,23 @@ 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 + ''' + # CVE-2017-1000117 block all SSH URLs (supported in git >= 2.3) + config = ['-c', 'core.sshCommand=false'] + envs.update({ + 'GIT_TERMINAL_PROMPT': '0', + 'GIT_SSH': 'false', # for git < 2.3 + 'SVN_SSH': 'false', + }) + return FDroidPopen(['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 = ['git', 'svn', 'clone'] + gitsvn_args = ['svn', 'clone'] if ';' in self.remote: remote_split = self.remote.split(';') for i in remote_split[1:]: @@ -953,13 +986,13 @@ elif i.startswith('branches='): gitsvn_args.extend(['-b', i[9:]]) gitsvn_args.extend([remote_split[0], self.local]) - p = FDroidPopen(gitsvn_args, output=False) + p = self.git(gitsvn_args, output=False) if p.returncode != 0: self.clone_failed = True raise VCSException("Git svn clone failed", p.output) else: gitsvn_args.extend([self.remote, self.local]) - p = FDroidPopen(gitsvn_args, output=False) + p = self.git(gitsvn_args, output=False) if p.returncode != 0: self.clone_failed = True raise VCSException("Git svn clone failed", p.output) @@ -967,20 +1000,20 @@ else: self.checkrepo() # Discard any working tree changes - p = FDroidPopen(['git', 'reset', '--hard'], cwd=self.local, output=False) + 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 = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False) + 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 = FDroidPopen(['git', 'svn', 'fetch'], cwd=self.local, output=False) + p = self.git(['svn', 'fetch'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Git svn fetch failed") - p = FDroidPopen(['git', 'svn', 'rebase'], cwd=self.local, output=False) + 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 @@ -990,7 +1023,7 @@ nospaces_rev = rev.replace(' ', '%20') # Try finding a svn tag for treeish in ['origin/', '']: - p = FDroidPopen(['git', 'checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False) + p = self.git(['checkout', treeish + 'tags/' + nospaces_rev], cwd=self.local, output=False) if p.returncode == 0: break if p.returncode != 0: @@ -1011,7 +1044,7 @@ svn_rev = svn_rev if svn_rev[0] == 'r' else 'r' + svn_rev - p = FDroidPopen(['git', 'svn', 'find-rev', '--before', svn_rev, treeish], cwd=self.local, output=False) + 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: @@ -1019,17 +1052,17 @@ if p.returncode != 0 or not git_rev: # Try a plain git checkout as a last resort - p = FDroidPopen(['git', 'checkout', rev], cwd=self.local, output=False) + 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 = FDroidPopen(['git', 'checkout', git_rev], cwd=self.local, output=False) + 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 = FDroidPopen(['git', 'clean', '-dffx'], cwd=self.local, output=False) + p = self.git(['clean', '-dffx'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException(_("Git clean failed"), p.output) @@ -1058,7 +1091,7 @@ def gotorevisionx(self, rev): if not os.path.exists(self.local): - p = FDroidPopen(['hg', 'clone', self.remote, self.local], output=False) + p = FDroidPopen(['hg', 'clone', '--ssh', 'false', self.remote, self.local], output=False) if p.returncode != 0: self.clone_failed = True raise VCSException("Hg clone failed", p.output) @@ -1071,7 +1104,7 @@ 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'], cwd=self.local, output=False) + p = FDroidPopen(['hg', 'pull', '--ssh', 'false'], cwd=self.local, output=False) if p.returncode != 0: raise VCSException("Hg pull failed", p.output) self.refreshed = True @@ -1106,29 +1139,36 @@ 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 = FDroidPopen(['bzr', 'branch', self.remote, self.local], output=False) + 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 = FDroidPopen(['bzr', 'clean-tree', '--force', '--unknown', '--ignored'], cwd=self.local, output=False) + 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 = FDroidPopen(['bzr', 'pull'], cwd=self.local, output=False) + 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 = FDroidPopen(['bzr', 'revert'] + revargs, cwd=self.local, output=False) + 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 = FDroidPopen(['bzr', 'tags'], cwd=self.local, output=False) + p = self.bzr(['tags'], cwd=self.local, output=False) return [tag.split(' ')[0].strip() for tag in p.output.splitlines()] @@ -1299,27 +1339,63 @@ vercode = None package = None + flavour = 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 - # Grab first occurence of each to avoid running into - # alternative flavours and builds. - 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) + + 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 + + matches = vnsearch_g(line) + if matches: + version = matches.group(2) + + 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) @@ -1716,6 +1792,23 @@ 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 @@ -1744,6 +1837,7 @@ 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): @@ -1866,6 +1960,22 @@ .format(apkfilename=apkfile)) +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 @@ -1918,6 +2028,7 @@ 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) @@ -2278,7 +2389,7 @@ """ with tempfile.TemporaryDirectory() as tmpdir: tmp_apk = os.path.join(tmpdir, 'tmp.apk') - os.rename(signed_apk, 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(): @@ -2339,6 +2450,40 @@ 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) + + p = SdkToolsPopen(['zipalign', '-v', '4', unsigned_path, signed_path]) + if p.returncode != 0: + raise BuildException(_("Failed to zipalign application")) + os.remove(unsigned_path) + + def verify_apks(signed_apk, unsigned_apk, tmp_dir): """Verify that two apks are the same @@ -2414,8 +2559,16 @@ """ - if subprocess.call([config['jarsigner'], '-strict', '-verify', jar]) != 4: - raise VerificationException(_("The repository's index 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): @@ -2431,14 +2584,24 @@ args = [config['apksigner'], 'verify'] if min_sdk_version: args += ['--min-sdk-version=' + min_sdk_version] - return subprocess.call(args + [apk]) == 0 + 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: - logging.warning("Using Java's jarsigner, not recommended for verifying APKs! Use apksigner") + 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: - pass + except Exception as e: + logging.error(e) return False @@ -2459,8 +2622,23 @@ with open(_java_security, 'w') as fp: fp.write('jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024') - return subprocess.call([config['jarsigner'], '-J-Djava.security.properties=' + _java_security, - '-strict', '-verify', apk]) == 4 + 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 + + logging.error(_('Old APK signature failed to verify: {path}').format(path=apk) + + '\n' + output.decode('utf-8')) + return False apk_badchars = re.compile('''[/ :;'"]''') diff -Nru fdroidserver-0.9.1/fdroidserver/exception.py fdroidserver-1.0.0/fdroidserver/exception.py --- fdroidserver-0.9.1/fdroidserver/exception.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/exception.py 2017-12-06 21:46:17.000000000 +0000 @@ -39,6 +39,10 @@ pass +class NoSubmodulesException(VCSException): + pass + + class BuildException(FDroidException): pass diff -Nru fdroidserver-0.9.1/fdroidserver/index.py fdroidserver-1.0.0/fdroidserver/index.py --- fdroidserver-0.9.1/fdroidserver/index.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/index.py 2017-12-14 15:54:01.000000000 +0000 @@ -431,6 +431,7 @@ 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 @@ -691,6 +692,7 @@ jar = zipfile.ZipFile(fp) # verify that the JAR signature is valid + logging.debug(_('Verifying index signature:')) common.verify_jar_signature(fp.name) # get public key and its fingerprint from JAR diff -Nru fdroidserver-0.9.1/fdroidserver/init.py fdroidserver-1.0.0/fdroidserver/init.py --- fdroidserver-0.9.1/fdroidserver/init.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/init.py 2017-12-06 21:46:17.000000000 +0000 @@ -144,7 +144,7 @@ if os.path.isfile(os.path.join(d, 'aapt')): aapt = os.path.join(d, 'aapt') break - if os.path.isfile(aapt): + 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 @@ -178,6 +178,7 @@ + '" 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) @@ -211,7 +212,16 @@ flags=re.MULTILINE) with open('opensc-fdroid.cfg', 'w') as f: f.write(opensc_fdroid) - elif not os.path.exists(keystore): + 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 diff -Nru fdroidserver-0.9.1/fdroidserver/lint.py fdroidserver-1.0.0/fdroidserver/lint.py --- fdroidserver-0.9.1/fdroidserver/lint.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/lint.py 2017-12-14 15:54:01.000000000 +0000 @@ -42,7 +42,15 @@ 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'), ] @@ -52,13 +60,59 @@ 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('t.co'), - forbid_shortener('ur1.ca'), forbid_shortener('is.gd'), - forbid_shortener('bit.ly'), + 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 + [ @@ -81,6 +135,8 @@ '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': [ @@ -101,17 +157,13 @@ (re.compile(r'.*\s$'), _("Unnecessary trailing space")), ], - 'Description': [ + 'Description': https_enforcings + http_url_shorteners + [ (re.compile(r'\s*[*#][^ .]'), _("Invalid bulleted list")), (re.compile(r'^\s'), _("Unnecessary leading space")), (re.compile(r'.*\s$'), _("Unnecessary trailing space")), - (re.compile(r'.*([^[]|^)\[[^:[\]]+( |\]|$)'), - _("Invalid link - use [http://foo.bar Link title] or [http://foo.bar]")), - (re.compile(r'(^|.* )https?://[^ ]+'), - _("Unlinkified link - use [http://foo.bar Link title] or [http://foo.bar]")), ], } diff -Nru fdroidserver-0.9.1/fdroidserver/metadata.py fdroidserver-1.0.0/fdroidserver/metadata.py --- fdroidserver-0.9.1/fdroidserver/metadata.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/metadata.py 2017-12-28 22:07:26.000000000 +0000 @@ -26,6 +26,7 @@ import textwrap import io import yaml +from collections import OrderedDict # use libyaml if it is available try: from yaml import CLoader @@ -69,6 +70,7 @@ 'Changelog', 'Donate', 'FlattrID', + 'LiberapayID', 'Bitcoin', 'Litecoin', 'Name', @@ -119,6 +121,7 @@ self.Changelog = '' self.Donate = None self.FlattrID = None + self.LiberapayID = None self.Bitcoin = None self.Litecoin = None self.Name = None @@ -390,6 +393,10 @@ r'^[0-9a-z]+$', ['FlattrID']), + FieldValidator("Liberapay ID", + r'^[0-9]+$', + ['LiberapayID']), + FieldValidator("HTTP link", r'^http[s]?://', ["WebSite", "SourceCode", "IssueTracker", "Changelog", "Donate"]), @@ -703,40 +710,57 @@ srclibs[srclibname] = parse_srclib(metadatapath) -def read_metadata(xref=True, check_vcs=[]): - """ - Read all metadata. Returns a list of 'app' objects (which are dictionaries as - returned by the parse_txt_metadata function. +def read_metadata(xref=True, check_vcs=[], refresh=True, sort_by_time=False): + """Return a list of App instances sorted newest first + + This reads all of the metadata files in a 'data' repository, then + builds a list of App instances from those files. The list is + sorted based on creation time, newest first. Most of the time, + the newer files are the most interesting. + + If there are multiple metadata files for a single appid, then the first + file that is parsed wins over all the others, and the rest throw an + exception. So the original .txt format is parsed first, at least until + newer formats stabilize. check_vcs is the list of packageNames to check for .fdroid.yml in source + """ # Always read the srclibs before the apps, since they can use a srlib as # their source repository. read_srclibs() - apps = {} + apps = OrderedDict() for basedir in ('metadata', 'tmp'): if not os.path.exists(basedir): os.makedirs(basedir) - # If there are multiple metadata files for a single appid, then the first - # file that is parsed wins over all the others, and the rest throw an - # exception. So the original .txt format is parsed first, at least until - # newer formats stabilize. - - for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt')) - + glob.glob(os.path.join('metadata', '*.json')) - + glob.glob(os.path.join('metadata', '*.yml')) - + glob.glob('.fdroid.txt') - + glob.glob('.fdroid.json') - + glob.glob('.fdroid.yml')): + metadatafiles = (glob.glob(os.path.join('metadata', '*.txt')) + + glob.glob(os.path.join('metadata', '*.json')) + + glob.glob(os.path.join('metadata', '*.yml')) + + glob.glob('.fdroid.txt') + + glob.glob('.fdroid.json') + + glob.glob('.fdroid.yml')) + + if sort_by_time: + entries = ((os.stat(path).st_mtime, path) for path in metadatafiles) + metadatafiles = [] + for _ignored, path in sorted(entries, reverse=True): + metadatafiles.append(path) + else: + # most things want the index alpha sorted for stability + metadatafiles = sorted(metadatafiles) + + for metadatapath in metadatafiles: + if metadatapath == '.fdroid.txt': + warn_or_exception(_('.fdroid.txt is not supported! Convert to .fdroid.yml or .fdroid.json.')) packageName, _ignored = fdroidserver.common.get_extension(os.path.basename(metadatapath)) if packageName in apps: warn_or_exception(_("Found multiple metadata files for {appid}") .format(path=packageName)) - app = parse_metadata(metadatapath, packageName in check_vcs) + app = parse_metadata(metadatapath, packageName in check_vcs, refresh) check_metadata(app) apps[app.id] = app @@ -920,7 +944,7 @@ warn_or_exception(_("Invalid boolean '%s'") % s) -def parse_metadata(metadatapath, check_vcs=False): +def parse_metadata(metadatapath, check_vcs=False, refresh=True): '''parse metadata file, optionally checking the git repo for metadata first''' _ignored, ext = fdroidserver.common.get_extension(metadatapath) @@ -954,7 +978,7 @@ if not os.path.isfile(metadata_in_repo): vcs, build_dir = fdroidserver.common.setup_vcs(app) if isinstance(vcs, fdroidserver.common.vcs_git): - vcs.gotorevision('HEAD') # HEAD since we can't know where else to go + vcs.gotorevision('HEAD', refresh) # HEAD since we can't know where else to go if os.path.isfile(metadata_in_repo): logging.debug('Including metadata from ' + metadata_in_repo) # do not include fields already provided by main metadata file @@ -1120,6 +1144,7 @@ 'Changelog', 'Donate', 'FlattrID', + 'LiberapayID', 'Bitcoin', 'Litecoin', '\n', @@ -1411,6 +1436,7 @@ w_field_nonempty('Changelog') w_field_nonempty('Donate') w_field_nonempty('FlattrID') + w_field_nonempty('LiberapayID') w_field_nonempty('Bitcoin') w_field_nonempty('Litecoin') mf.write('\n') @@ -1529,5 +1555,5 @@ def add_metadata_arguments(parser): '''add common command line flags related to metadata processing''' - parser.add_argument("-W", default='error', - help=_("force errors to be warnings, or ignore")) + parser.add_argument("-W", choices=['error', 'warn', 'ignore'], default='error', + help=_("force metadata errors (default) to be warnings, or to be ignored.")) diff -Nru fdroidserver-0.9.1/fdroidserver/mirror.py fdroidserver-1.0.0/fdroidserver/mirror.py --- fdroidserver-0.9.1/fdroidserver/mirror.py 1970-01-01 00:00:00.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/mirror.py 2017-11-30 13:03:04.000000000 +0000 @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 + +import ipaddress +import logging +import os +import posixpath +import socket +import subprocess +import sys +from argparse import ArgumentParser +import urllib.parse + +from . import _ +from . import common +from . import index +from . import update + +options = None + + +def _run_wget(path, urls): + if options.verbose: + verbose = '--verbose' + else: + verbose = '--no-verbose' + + if not urls: + return + logging.debug(_('Running wget in {path}').format(path=path)) + os.makedirs(path, exist_ok=True) + os.chdir(path) + urls_file = '.fdroid-mirror-wget-input-file' + with open(urls_file, 'w') as fp: + for url in urls: + fp.write(url.split('?')[0] + '\n') # wget puts query string in the filename + subprocess.call(['wget', verbose, '--continue', '--user-agent="fdroid mirror"', + '--input-file=' + urls_file]) + os.remove(urls_file) + + +def main(): + global options + + parser = ArgumentParser(usage=_("%(prog)s [options] url")) + common.setup_global_opts(parser) + parser.add_argument("url", nargs='?', + help=_('Base URL to mirror, can include the index signing key ' + + 'using the query string: ?fingerprint=')) + parser.add_argument("--archive", action='store_true', default=False, + help=_("Also mirror the full archive section")) + parser.add_argument("--output-dir", default=None, + help=_("The directory to write the mirror to")) + options = parser.parse_args() + + if options.url is None: + logging.error(_('A URL is required as an argument!') + '\n') + parser.print_help() + sys.exit(1) + + scheme, hostname, path, params, query, fragment = urllib.parse.urlparse(options.url) + fingerprint = urllib.parse.parse_qs(query).get('fingerprint') + + def _append_to_url_path(*args): + '''Append the list of path components to URL, keeping the rest the same''' + newpath = posixpath.join(path, *args) + return urllib.parse.urlunparse((scheme, hostname, newpath, params, query, fragment)) + + if fingerprint: + config = common.read_config(options) + if not ('jarsigner' in config or 'apksigner' in config): + logging.error(_('Java JDK not found! Install in standard location or set java_paths!')) + sys.exit(1) + + def _get_index(section, etag=None): + url = _append_to_url_path(section) + return index.download_repo_index(url, etag=etag) + else: + def _get_index(section, etag=None): + import io + import json + import zipfile + from . import net + url = _append_to_url_path(section, 'index-v1.jar') + content, etag = net.http_get(url) + with zipfile.ZipFile(io.BytesIO(content)) as zip: + jsoncontents = zip.open('index-v1.json').read() + data = json.loads(jsoncontents.decode('utf-8')) + return data, etag + + ip = None + try: + ip = ipaddress.ip_address(hostname) + except ValueError: + pass + if hostname == 'f-droid.org' \ + or (ip is not None and hostname in socket.gethostbyname_ex('f-droid.org')[2]): + print(_('ERROR: this command should never be used to mirror f-droid.org!\n' + 'A full mirror of f-droid.org requires more than 200GB.')) + sys.exit(1) + + path = path.rstrip('/') + if path.endswith('repo') or path.endswith('archive'): + logging.warning(_('Do not include "{path}" in URL!') + .format(path=path.split('/')[-1])) + elif not path.endswith('fdroid'): + logging.warning(_('{url} does not end with "fdroid", check the URL path!') + .format(url=options.url)) + + icondirs = ['icons', ] + for density in update.screen_densities: + icondirs.append('icons-' + density) + + if options.output_dir: + basedir = options.output_dir + else: + basedir = os.path.join(os.getcwd(), hostname, path.strip('/')) + os.makedirs(basedir, exist_ok=True) + + if options.archive: + sections = ('repo', 'archive') + else: + sections = ('repo', ) + + for section in sections: + sectiondir = os.path.join(basedir, section) + + data, etag = _get_index(section) + + os.makedirs(sectiondir, exist_ok=True) + os.chdir(sectiondir) + for icondir in icondirs: + os.makedirs(os.path.join(sectiondir, icondir), exist_ok=True) + + urls = [] + for packageName, packageList in data['packages'].items(): + for package in packageList: + to_fetch = [] + for k in ('apkName', 'srcname'): + if k in package: + to_fetch.append(package[k]) + elif k == 'apkName': + logging.error(_('{appid} is missing {name}') + .format(appid=package['packageName'], name=k)) + for f in to_fetch: + if not os.path.exists(f) \ + or (f.endswith('.apk') and os.path.getsize(f) != package['size']): + urls.append(_append_to_url_path(section, f)) + urls.append(_append_to_url_path(section, f + '.asc')) + _run_wget(sectiondir, urls) + + for app in data['apps']: + localized = app.get('localized') + if localized: + for locale, d in localized.items(): + urls = [] + components = (section, app['packageName'], locale) + for k in update.GRAPHIC_NAMES: + f = d.get(k) + if f: + filepath_tuple = components + (f, ) + urls.append(_append_to_url_path(*filepath_tuple)) + _run_wget(os.path.join(basedir, *components), urls) + for k in update.SCREENSHOT_DIRS: + urls = [] + filelist = d.get(k) + if filelist: + components = (section, app['packageName'], locale, k) + for f in filelist: + filepath_tuple = components + (f, ) + urls.append(_append_to_url_path(*filepath_tuple)) + _run_wget(os.path.join(basedir, *components), urls) + + urls = dict() + for app in data['apps']: + if 'icon' not in app: + logging.error(_('no "icon" in {appid}').format(appid=app['packageName'])) + continue + icon = app['icon'] + for icondir in icondirs: + url = _append_to_url_path(section, icondir, icon) + if icondir not in urls: + urls[icondir] = [] + urls[icondir].append(url) + + for icondir in icondirs: + _run_wget(os.path.join(basedir, section, icondir), urls[icondir]) + + +if __name__ == "__main__": + main() diff -Nru fdroidserver-0.9.1/fdroidserver/nightly.py fdroidserver-1.0.0/fdroidserver/nightly.py --- fdroidserver-0.9.1/fdroidserver/nightly.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/nightly.py 2017-12-28 22:07:26.000000000 +0000 @@ -28,6 +28,7 @@ import subprocess import sys import tempfile +import yaml from urllib.parse import urlparse from argparse import ArgumentParser @@ -46,13 +47,15 @@ NIGHTLY = '-nightly' -def _ssh_key_from_debug_keystore(): +def _ssh_key_from_debug_keystore(keystore=KEYSTORE_FILE): tmp_dir = tempfile.mkdtemp(prefix='.') privkey = os.path.join(tmp_dir, '.privkey') key_pem = os.path.join(tmp_dir, '.key.pem') p12 = os.path.join(tmp_dir, '.keystore.p12') - subprocess.check_call([common.config['keytool'], '-importkeystore', - '-srckeystore', KEYSTORE_FILE, '-srcalias', KEY_ALIAS, + _config = dict() + common.fill_config_defaults(_config) + subprocess.check_call([_config['keytool'], '-importkeystore', + '-srckeystore', keystore, '-srcalias', KEY_ALIAS, '-srcstorepass', PASSWORD, '-srckeypass', PASSWORD, '-destkeystore', p12, '-destalias', KEY_ALIAS, '-deststorepass', PASSWORD, '-destkeypass', PASSWORD, @@ -67,8 +70,9 @@ rsakey = paramiko.RSAKey.from_private_key_file(privkey) fingerprint = base64.b64encode(hashlib.sha256(rsakey.asbytes()).digest()).decode('ascii').rstrip('=') - ssh_private_key_file = os.path.join(tmp_dir, 'debug_keystore_' + fingerprint + '_id_rsa') - os.rename(privkey, ssh_private_key_file) + ssh_private_key_file = os.path.join(tmp_dir, 'debug_keystore_' + + fingerprint.replace('/', '_') + '_id_rsa') + shutil.move(privkey, ssh_private_key_file) pub = rsakey.get_name() + ' ' + rsakey.get_base64() + ' ' + ssh_private_key_file with open(ssh_private_key_file + '.pub', 'w') as fp: @@ -83,6 +87,8 @@ parser = ArgumentParser(usage="%(prog)s") common.setup_global_opts(parser) + parser.add_argument("--keystore", default=KEYSTORE_FILE, + help=_("Specify which debug keystore file to use.")) parser.add_argument("--show-secret-var", action="store_true", default=False, help=_("Print the secret variable to the terminal for easy copy/paste")) parser.add_argument("--file", default='app/build/outputs/apk/*.apk', @@ -91,7 +97,6 @@ help=_("Don't use rsync checksums")) # TODO add --with-btlog options = parser.parse_args() - common.read_config(None) # force a tighter umask since this writes private key material umask = os.umask(0o077) @@ -155,6 +160,7 @@ repo_url = repo_base + '/repo' git_mirror_path = os.path.join(repo_basedir, 'git-mirror') git_mirror_repodir = os.path.join(git_mirror_path, 'fdroid', 'repo') + git_mirror_metadatadir = os.path.join(git_mirror_path, 'fdroid', 'metadata') if not os.path.isdir(git_mirror_repodir): logging.debug(_('cloning {url}').format(url=clone_url)) try: @@ -186,20 +192,22 @@ mirror_git_repo.git.add(all=True) mirror_git_repo.index.commit("update README") - icon_path = os.path.join(repo_basedir, 'icon.png') + icon_path = os.path.join(git_mirror_path, 'icon.png') try: import qrcode - img = qrcode.make('Some data here') - with open(icon_path, 'wb') as fp: - fp.write(img) + qrcode.make(repo_url).save(icon_path) except Exception: exampleicon = os.path.join(common.get_examples_dir(), 'fdroid-icon.png') shutil.copy(exampleicon, icon_path) mirror_git_repo.git.add(all=True) mirror_git_repo.index.commit("update repo/website icon") + shutil.copy(icon_path, repo_basedir) os.chdir(repo_basedir) - common.local_rsync(options, git_mirror_repodir + '/', 'repo/') + if os.path.isdir(git_mirror_repodir): + common.local_rsync(options, git_mirror_repodir + '/', 'repo/') + if os.path.isdir(git_mirror_metadatadir): + common.local_rsync(options, git_mirror_metadatadir + '/', 'metadata/') ssh_private_key_file = _ssh_key_from_debug_keystore() # this is needed for GitPython to find the SSH key @@ -230,6 +238,8 @@ with open('config.py', 'w') as fp: fp.write(config) os.chmod('config.py', 0o600) + config = common.read_config(options) + common.assert_config_keystore(config) for root, dirs, files in os.walk(cibase): for d in ('fdroid', '.git', '.gradle'): @@ -238,12 +248,14 @@ for f in files: if f.endswith('-debug.apk'): apkfilename = os.path.join(root, f) - logging.debug(_('copying {apkfilename} into {path}') - .format(apkfilename=apkfilename, path=repodir)) + logging.debug(_('Striping mystery signature from {apkfilename}') + .format(apkfilename=apkfilename)) destapk = os.path.join(repodir, os.path.basename(f)) - shutil.copyfile(apkfilename, destapk) - shutil.copystat(apkfilename, destapk) - os.chmod(destapk, 0o644) + os.chmod(apkfilename, 0o644) + logging.debug(_('Resigning {apkfilename} with provided debug.keystore') + .format(apkfilename=os.path.basename(apkfilename))) + common.apk_strip_signatures(apkfilename, strip_manifest=True) + common.sign_apk(apkfilename, destapk, KEY_ALIAS) if options.verbose: logging.debug(_('attempting bare ssh connection to test deploy key:')) @@ -254,7 +266,23 @@ except subprocess.CalledProcessError: pass - subprocess.check_call(['fdroid', 'update', '--rename-apks', '--verbose'], cwd=repo_basedir) + app_url = clone_url[:-len(NIGHTLY)] + template = dict() + template['AuthorName'] = clone_url.split('/')[4] + template['AuthorWebSite'] = '/'.join(clone_url.split('/')[:4]) + template['Categories'] = ['nightly'] + template['SourceCode'] = app_url + template['IssueTracker'] = app_url + '/issues' + template['Summary'] = 'Nightly build of ' + urlparse(app_url).path[1:] + template['Description'] = template['Summary'] + with open('template.yml', 'w') as fp: + yaml.dump(template, fp) + + subprocess.check_call(['fdroid', 'update', '--rename-apks', '--create-metadata', '--verbose'], + cwd=repo_basedir) + common.local_rsync(options, repo_basedir + '/metadata/', git_mirror_metadatadir + '/') + mirror_git_repo.git.add(all=True) + mirror_git_repo.index.commit("update app metadata") try: subprocess.check_call(['fdroid', 'server', 'update', '--verbose'], cwd=repo_basedir) except subprocess.CalledProcessError: @@ -265,20 +293,30 @@ shutil.rmtree(os.path.dirname(ssh_private_key_file)) else: + if not os.path.isfile(options.keystore): + androiddir = os.path.dirname(options.keystore) + if not os.path.exists(androiddir): + os.mkdir(androiddir) + logging.info(_('created {path}').format(path=androiddir)) + logging.error(_('{path} does not exist! Create it by running:').format(path=options.keystore) + + '\n keytool -genkey -v -keystore ' + options.keystore + ' -storepass android \\' + + '\n -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 \\' + + '\n -dname "CN=Android Debug,O=Android,C=US"') + sys.exit(1) ssh_dir = os.path.join(os.getenv('HOME'), '.ssh') os.makedirs(os.path.dirname(ssh_dir), exist_ok=True) - privkey = _ssh_key_from_debug_keystore() + privkey = _ssh_key_from_debug_keystore(options.keystore) ssh_private_key_file = os.path.join(ssh_dir, os.path.basename(privkey)) - os.rename(privkey, ssh_private_key_file) - os.rename(privkey + '.pub', ssh_private_key_file + '.pub') + shutil.move(privkey, ssh_private_key_file) + shutil.move(privkey + '.pub', ssh_private_key_file + '.pub') if shutil.rmtree.avoids_symlink_attacks: shutil.rmtree(os.path.dirname(privkey)) if options.show_secret_var: - with open(KEYSTORE_FILE, 'rb') as fp: + with open(options.keystore, 'rb') as fp: debug_keystore = base64.standard_b64encode(fp.read()).decode('ascii') print(_('\n{path} encoded for the DEBUG_KEYSTORE secret variable:') - .format(path=KEYSTORE_FILE)) + .format(path=options.keystore)) print(debug_keystore) os.umask(umask) diff -Nru fdroidserver-0.9.1/fdroidserver/publish.py fdroidserver-1.0.0/fdroidserver/publish.py --- fdroidserver-0.9.1/fdroidserver/publish.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/publish.py 2017-12-06 21:46:17.000000000 +0000 @@ -339,6 +339,7 @@ unsigned_dir, output_dir)) + # TODO replace below with common.sign_apk() once it has proven stable # Sign the application... p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'], '-storepass:env', 'FDROID_KEY_STORE_PASS', diff -Nru fdroidserver-0.9.1/fdroidserver/scanner.py fdroidserver-1.0.0/fdroidserver/scanner.py --- fdroidserver-0.9.1/fdroidserver/scanner.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/scanner.py 2017-12-20 08:46:43.000000000 +0000 @@ -69,9 +69,18 @@ ] } + whitelisted = [ + 'firebase-jobdispatcher', # https://github.com/firebase/firebase-jobdispatcher-android/blob/master/LICENSE + 'com.firebaseui', # https://github.com/firebase/FirebaseUI-Android/blob/master/LICENSE + 'geofire-android' # https://github.com/firebase/geofire-java/blob/master/LICENSE + ] + + def is_whitelisted(s): + return any(wl in s for wl in whitelisted) + def suspects_found(s): for n, r in usual_suspects.items(): - if r.match(s): + if r.match(s) and not is_whitelisted(s): yield n gradle_mavenrepo = re.compile(r'maven *{ *(url)? *[\'"]?([^ \'"]*)[\'"]?') @@ -198,7 +207,7 @@ elif ext == 'jar': for name in suspects_found(curfile): - count += handleproblem('usual supect \'%s\'' % name, path_in_build_dir, filepath) + count += handleproblem('usual suspect \'%s\'' % name, path_in_build_dir, filepath) if curfile == 'gradle-wrapper.jar': removeproblem('gradle-wrapper.jar', path_in_build_dir, filepath) else: @@ -224,7 +233,7 @@ for i, line in enumerate(lines): if is_used_by_gradle(line): for name in suspects_found(line): - count += handleproblem('usual supect \'%s\' at line %d' % (name, i + 1), path_in_build_dir, filepath) + count += handleproblem('usual suspect \'%s\' at line %d' % (name, i + 1), path_in_build_dir, filepath) noncomment_lines = [l for l in lines if not common.gradle_comment.match(l)] joined = re.sub(r'[\n\r\s]+', ' ', ' '.join(noncomment_lines)) for m in gradle_mavenrepo.finditer(joined): diff -Nru fdroidserver-0.9.1/fdroidserver/server.py fdroidserver-1.0.0/fdroidserver/server.py --- fdroidserver-0.9.1/fdroidserver/server.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/server.py 2017-11-30 12:43:20.000000000 +0000 @@ -38,6 +38,9 @@ BINARY_TRANSPARENCY_DIR = 'binary_transparency' +AUTO_S3CFG = '.fdroid-server-update-s3cfg' +USER_S3CFG = 's3cfg' + def update_awsbucket(repo_section): ''' @@ -72,12 +75,17 @@ logging.debug(_('Using s3cmd to sync with: {url}') .format(url=config['awsbucket'])) - configfilename = '.s3cfg' - fd = os.open(configfilename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0o600) - os.write(fd, '[default]\n'.encode('utf-8')) - os.write(fd, ('access_key = ' + config['awsaccesskeyid'] + '\n').encode('utf-8')) - os.write(fd, ('secret_key = ' + config['awssecretkey'] + '\n').encode('utf-8')) - os.close(fd) + if os.path.exists(USER_S3CFG): + logging.info(_('Using "{path}" for configuring s3cmd.').format(path=USER_S3CFG)) + configfilename = USER_S3CFG + else: + fd = os.open(AUTO_S3CFG, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0o600) + logging.debug(_('Creating "{path}" for configuring s3cmd.').format(path=AUTO_S3CFG)) + os.write(fd, '[default]\n'.encode('utf-8')) + os.write(fd, ('access_key = ' + config['awsaccesskeyid'] + '\n').encode('utf-8')) + os.write(fd, ('secret_key = ' + config['awssecretkey'] + '\n').encode('utf-8')) + os.close(fd) + configfilename = AUTO_S3CFG s3bucketurl = 's3://' + config['awsbucket'] s3cmd = [config['s3cmd'], '--config=' + configfilename] @@ -151,6 +159,10 @@ _('To use awsbucket, awssecretkey and awsaccesskeyid must also be set in config.py!')) awsbucket = config['awsbucket'] + if os.path.exists(USER_S3CFG): + raise FDroidException(_('"{path}" exists but s3cmd is not installed!') + .format(path=USER_S3CFG)) + cls = get_driver(Provider.S3) driver = cls(config['awsaccesskeyid'], config['awssecretkey']) try: @@ -501,7 +513,7 @@ with open(outputfilename, 'w') as fp: json.dump(response, fp, indent=2, sort_keys=True) - if response.get('positives') > 0: + if response.get('positives', 0) > 0: logging.warning(repofilename + ' has been flagged by virustotal ' + str(response['positives']) + ' times:' + '\n\t' + response['permalink']) diff -Nru fdroidserver-0.9.1/fdroidserver/update.py fdroidserver-1.0.0/fdroidserver/update.py --- fdroidserver-0.9.1/fdroidserver/update.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/update.py 2018-01-03 15:57:25.000000000 +0000 @@ -29,13 +29,13 @@ import hashlib import pickle import time -from datetime import datetime, timedelta +from datetime import datetime from argparse import ArgumentParser import collections from binascii import hexlify -from PIL import Image +from PIL import Image, PngImagePlugin import logging from . import _ @@ -84,6 +84,8 @@ SCREENSHOT_DIRS = ('phoneScreenshots', 'sevenInchScreenshots', 'tenInchScreenshots', 'tvScreenshots', 'wearScreenshots') +BLANK_PNG_INFO = PngImagePlugin.PngInfo() + def dpi_to_px(density): return (int(density) * 48) / 160 @@ -138,7 +140,7 @@ requiresroot = 'Yes' else: requiresroot = 'No' - wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % ( + wikidata += '{{App|id=%s|name=%s|added=%s|lastupdated=%s|source=%s|tracker=%s|web=%s|changelog=%s|donate=%s|flattr=%s|liberapay=%s|bitcoin=%s|litecoin=%s|license=%s|root=%s|author=%s|email=%s}}\n' % ( appid, app.Name, app.added.strftime('%Y-%m-%d') if app.added else '', @@ -149,6 +151,7 @@ app.Changelog, app.Donate, app.FlattrID, + app.LiberapayID, app.Bitcoin, app.Litecoin, app.License, @@ -370,7 +373,8 @@ im.thumbnail((size, size), Image.ANTIALIAS) logging.debug("%s was too large at %s - new size is %s" % ( iconpath, oldsize, im.size)) - im.save(iconpath, "PNG") + im.save(iconpath, "PNG", optimize=True, + pnginfo=BLANK_PNG_INFO, icc_profile=None) except Exception as e: logging.error(_("Failed resizing {path}: {error}".format(path=iconpath, error=e))) @@ -495,14 +499,25 @@ Checks whether there are more than one classes.dex or AndroidManifest.xml files, which is invalid and an essential part of the "Master Key" attack. - http://www.saurik.com/id/17 + + Janus is similar to Master Key but is perhaps easier to scan for. + https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures """ + found_vuln = False + # statically load this pattern if not hasattr(has_known_vulnerability, "pattern"): has_known_vulnerability.pattern = re.compile(b'.*OpenSSL ([01][0-9a-z.-]+)') + with open(filename.encode(), 'rb') as fp: + first4 = fp.read(4) + if first4 != b'\x50\x4b\x03\x04': + raise FDroidException(_('{path} has bad file signature "{pattern}", possible Janus exploit!') + .format(path=filename, pattern=first4.decode().replace('\n', ' ')) + '\n' + + 'https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures') + files_in_apk = set() with zipfile.ZipFile(filename) as zf: for name in zf.namelist(): @@ -523,14 +538,15 @@ else: logging.warning(_('"{path}" contains outdated {name} ({version})') .format(path=filename, name=name, version=version)) - return True + found_vuln = True break elif name == 'AndroidManifest.xml' or name == 'classes.dex' or name.endswith('.so'): if name in files_in_apk: - return True + logging.warning(_('{apkfilename} has multiple {name} files, looks like Master Key exploit!') + .format(apkfilename=filename, name=name)) + found_vuln = True files_in_apk.add(name) - - return False + return found_vuln def insert_obbs(repodir, apps, apks): @@ -659,6 +675,35 @@ app[key] = text +def _strip_and_copy_image(inpath, outpath): + """Remove any metadata from image and copy it to new path + + Sadly, image metadata like EXIF can be used to exploit devices. + It is not used at all in the F-Droid ecosystem, so its much safer + just to remove it entirely. + + """ + + extension = common.get_extension(inpath)[1] + if os.path.isdir(outpath): + outpath = os.path.join(outpath, os.path.basename(inpath)) + if extension == 'png': + with open(inpath, 'rb') as fp: + in_image = Image.open(fp) + in_image.save(outpath, "PNG", optimize=True, + pnginfo=BLANK_PNG_INFO, icc_profile=None) + elif extension == 'jpg' or extension == 'jpeg': + with open(inpath, 'rb') as fp: + in_image = Image.open(fp) + data = list(in_image.getdata()) + out_image = Image.new(in_image.mode, in_image.size) + out_image.putdata(data) + out_image.save(outpath, "JPEG", optimize=True) + else: + raise FDroidException(_('Unsupported file type "{extension}" for repo graphic') + .format(extension=extension)) + + def copy_triple_t_store_metadata(apps): """Include store metadata from the app's source repo @@ -731,7 +776,7 @@ sourcefile = os.path.join(root, f) destfile = os.path.join(destdir, os.path.basename(f)) logging.debug('copying ' + sourcefile + ' ' + destfile) - shutil.copy(sourcefile, destfile) + _strip_and_copy_image(sourcefile, destfile) def insert_localized_app_metadata(apps): @@ -772,7 +817,8 @@ """ - sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*')) + sourcedirs = glob.glob(os.path.join('build', '[A-Za-z]*', 'src', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*')) + sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'fastlane', 'metadata', 'android', '[a-z][a-z]*')) sourcedirs += glob.glob(os.path.join('build', '[A-Za-z]*', 'metadata', '[a-z][a-z]*')) sourcedirs += glob.glob(os.path.join('metadata', '[A-Za-z]*', '[a-z][a-z]*')) @@ -787,6 +833,17 @@ continue locale = segments[-1] destdir = os.path.join('repo', packageName, locale) + + # flavours specified in build receipt + build_flavours = "" + if apps[packageName] and 'builds' in apps[packageName] and len(apps[packageName].builds) > 0\ + and 'gradle' in apps[packageName].builds[-1]: + build_flavours = apps[packageName].builds[-1].gradle + + if len(segments) >= 5 and segments[4] == "fastlane" and segments[3] not in build_flavours: + logging.debug("ignoring due to wrong flavour") + continue + for f in files: if f in ('description.txt', 'full_description.txt'): _set_localized_text_entry(apps[packageName], locale, 'description', @@ -817,7 +874,7 @@ if base in GRAPHIC_NAMES and extension in ALLOWED_EXTENSIONS: os.makedirs(destdir, mode=0o755, exist_ok=True) logging.debug('copying ' + os.path.join(root, f) + ' ' + destdir) - shutil.copy(os.path.join(root, f), destdir) + _strip_and_copy_image(os.path.join(root, f), destdir) for d in dirs: if d in SCREENSHOT_DIRS: if locale == 'images': @@ -829,7 +886,7 @@ screenshotdestdir = os.path.join(destdir, d) os.makedirs(screenshotdestdir, mode=0o755, exist_ok=True) logging.debug('copying ' + f + ' ' + screenshotdestdir) - shutil.copy(f, screenshotdestdir) + _strip_and_copy_image(f, screenshotdestdir) repofiles = sorted(glob.glob(os.path.join('repo', '[A-Za-z]*', '[a-z][a-z][A-Z-.@]*'))) for d in repofiles: @@ -1297,22 +1354,14 @@ apkzip = zipfile.ZipFile(apkfile, 'r') - # 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 manifest = apkzip.getinfo('AndroidManifest.xml') - if manifest.date_time[1] == 0: # month can't be zero - logging.debug(_('AndroidManifest.xml has no date')) - else: - dt_obj = datetime(*manifest.date_time) - checkdt = dt_obj - timedelta(1) - if datetime.today() < checkdt: - logging.warning('System clock is older than manifest in: ' - + apkfilename - + '\nSet clock to that time using:\n' - + 'sudo date -s "' + str(dt_obj) + '"') + # 1980-0-0 means zeroed out, any other invalid date should trigger a warning + if (1980, 0, 0) != manifest.date_time[0:3]: + try: + common.check_system_clock(datetime(*manifest.date_time), apkfilename) + except ValueError as e: + logging.warning(_("{apkfilename}'s AndroidManifest.xml has a bad date: ") + .format(apkfilename=apkfile) + str(e)) # extract icons from APK zip file iconfilename = "%s.%s.png" % (apk['packageName'], apk['versionCode']) @@ -1426,6 +1475,7 @@ icon_path = os.path.join(get_icon_dir(repo_dir, '0'), icon_filename) with open(icon_path, 'wb') as f: f.write(get_icon_bytes(apkzip, icon_src)) + im = None try: im = Image.open(icon_path) dpi = px_to_dpi(im.size[0]) @@ -1441,6 +1491,9 @@ except Exception as e: logging.warning(_("Failed reading {path}: {error}") .format(path=icon_path, error=e)) + finally: + if im and hasattr(im, 'close'): + im.close() if apk['icons']: apk['icon'] = icon_filename @@ -1477,7 +1530,8 @@ size = dpi_to_px(density) im.thumbnail((size, size), Image.ANTIALIAS) - im.save(icon_path, "PNG") + im.save(icon_path, "PNG", optimize=True, + pnginfo=BLANK_PNG_INFO, icc_profile=None) empty_densities.remove(density) except Exception as e: logging.warning("Invalid image file at %s: %s", last_icon_path, e) @@ -1669,7 +1723,7 @@ with open('template.yml') as f: metatxt = f.read() if 'name' in apk and apk['name'] != '': - metatxt = re.sub(r'^(((Auto)?Name|Summary):).*$', + metatxt = re.sub(r'''^(((Auto)?Name|Summary):)[ '"\.]*$''', r'\1 ' + apk['name'], metatxt, flags=re.IGNORECASE | re.MULTILINE) diff -Nru fdroidserver-0.9.1/fdroidserver/vmtools.py fdroidserver-1.0.0/fdroidserver/vmtools.py --- fdroidserver-0.9.1/fdroidserver/vmtools.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver/vmtools.py 2017-12-22 22:34:01.000000000 +0000 @@ -22,13 +22,14 @@ import math import json import tarfile -import time import shutil import subprocess import textwrap from .common import FDroidException from logging import getLogger +from fdroidserver import _ + logger = getLogger('fdroidserver-vmtools') @@ -53,7 +54,7 @@ if reset: logger.info('resetting buildserver by request') elif not vm.vagrant_uuid_okay(): - logger.info('resetting buildserver, bceause vagrant vm is not okay.') + logger.info('resetting buildserver, because vagrant vm is not okay.') reset = True elif not vm.snapshot_exists('fdroidclean'): logger.info("resetting buildserver, because snapshot 'fdroidclean' is not present.") @@ -192,8 +193,6 @@ def up(self, provision=True): try: self.vgrnt.up(provision=provision) - logger.info('...waiting a sec...') - time.sleep(10) self.srvuuid = self._vagrant_fetch_uuid() except subprocess.CalledProcessError as e: raise FDroidBuildVmException("could not bring up vm '%s'" % self.srvname) from e @@ -202,8 +201,6 @@ logger.info('suspending buildserver') try: self.vgrnt.suspend() - logger.info('...waiting a sec...') - time.sleep(10) except subprocess.CalledProcessError as e: raise FDroidBuildVmException("could not suspend vm '%s'" % self.srvname) from e @@ -350,16 +347,12 @@ # (eg. lookupByName only works on running VMs) try: _check_call(('virsh', '-c', 'qemu:///system', 'destroy', self.srvname)) - logger.info("...waiting a sec...") - time.sleep(10) except subprocess.CalledProcessError as e: logger.info("could not force libvirt domain '%s' off: %s", self.srvname, e) try: # libvirt python bindings do not support all flags required # for undefining domains correctly. _check_call(('virsh', '-c', 'qemu:///system', 'undefine', self.srvname, '--nvram', '--managed-save', '--remove-all-storage', '--snapshots-metadata')) - logger.info("...waiting a sec...") - time.sleep(10) except subprocess.CalledProcessError as e: logger.info("could not undefine libvirt domain '%s': %s", self.srvname, e) @@ -383,7 +376,9 @@ vol = storagePool.storageVolLookupByName(self.srvname + '.img') imagepath = vol.path() # TODO use a libvirt storage pool to ensure the img file is readable - _check_call(['sudo', '/bin/chmod', '-R', 'a+rX', '/var/lib/libvirt/images']) + if not os.access(imagepath, os.R_OK): + logger.warning(_('Cannot read "{path}"!').format(path=imagepath)) + _check_call(['sudo', '/bin/chmod', '-R', 'a+rX', '/var/lib/libvirt/images']) shutil.copy2(imagepath, 'box.img') _check_call(['qemu-img', 'rebase', '-p', '-b', '', 'box.img']) img_info_raw = _check_output(['qemu-img', 'info', '--output=json', 'box.img']) @@ -454,8 +449,6 @@ logger.info("creating snapshot '%s' for vm '%s'", snapshot_name, self.srvname) try: _check_call(['virsh', '-c', 'qemu:///system', 'snapshot-create-as', self.srvname, snapshot_name]) - logger.info('...waiting a sec...') - time.sleep(10) except subprocess.CalledProcessError as e: raise FDroidBuildVmException("could not cerate snapshot '%s' " "of libvirt vm '%s'" @@ -484,8 +477,6 @@ dom = self.conn.lookupByName(self.srvname) snap = dom.snapshotLookupByName(snapshot_name) dom.revertToSnapshot(snap) - logger.info('...waiting a sec...') - time.sleep(10) except libvirt.libvirtError as e: raise FDroidBuildVmException('could not revert domain \'%s\' to snapshot \'%s\'' % (self.srvname, snapshot_name)) from e @@ -501,8 +492,6 @@ logger.info("creating snapshot '%s' for vm '%s'", snapshot_name, self.srvname) try: _check_call(['VBoxManage', 'snapshot', self.srvuuid, 'take', 'fdroidclean'], cwd=self.srvdir) - logger.info('...waiting a sec...') - time.sleep(10) except subprocess.CalledProcessError as e: raise FDroidBuildVmException('could not cerate snapshot ' 'of virtualbox vm %s' diff -Nru fdroidserver-0.9.1/fdroidserver.egg-info/PKG-INFO fdroidserver-1.0.0/fdroidserver.egg-info/PKG-INFO --- fdroidserver-0.9.1/fdroidserver.egg-info/PKG-INFO 2017-11-27 19:09:53.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver.egg-info/PKG-INFO 2018-01-03 20:42:26.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: fdroidserver -Version: 0.9.1 +Version: 1.0.0 Summary: F-Droid Server Tools Home-page: https://f-droid.org Author: The F-Droid Project @@ -55,6 +55,55 @@ 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 `__ 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 `__ + and push this to a new branch of your fork. + + Alternatively `run them + locally `__ + 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 ~~~~~~~~~~~~~~ diff -Nru fdroidserver-0.9.1/fdroidserver.egg-info/requires.txt fdroidserver-1.0.0/fdroidserver.egg-info/requires.txt --- fdroidserver-0.9.1/fdroidserver.egg-info/requires.txt 2017-11-27 19:09:53.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver.egg-info/requires.txt 2018-01-03 20:42:26.000000000 +0000 @@ -8,6 +8,7 @@ pyasn1-modules python-vagrant PyYAML +qrcode ruamel.yaml >= 0.13 requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0 docker-py >= 1.9, < 2.0 diff -Nru fdroidserver-0.9.1/fdroidserver.egg-info/SOURCES.txt fdroidserver-1.0.0/fdroidserver.egg-info/SOURCES.txt --- fdroidserver-0.9.1/fdroidserver.egg-info/SOURCES.txt 2017-11-27 19:09:53.000000000 +0000 +++ fdroidserver-1.0.0/fdroidserver.egg-info/SOURCES.txt 2018-01-03 20:42:26.000000000 +0000 @@ -1,7 +1,6 @@ LICENSE MANIFEST.in README.rst -fd-commit fdroid makebuildserver setup.cfg @@ -45,6 +44,7 @@ fdroidserver/install.py fdroidserver/lint.py fdroidserver/metadata.py +fdroidserver/mirror.py fdroidserver/net.py fdroidserver/nightly.py fdroidserver/publish.py Binary files /tmp/tmptqOmCq/As4wx2_aTZ/fdroidserver-0.9.1/locale/bo/LC_MESSAGES/fdroidserver.mo and /tmp/tmptqOmCq/5S9s02wc9x/fdroidserver-1.0.0/locale/bo/LC_MESSAGES/fdroidserver.mo differ Binary files /tmp/tmptqOmCq/As4wx2_aTZ/fdroidserver-0.9.1/locale/de/LC_MESSAGES/fdroidserver.mo and /tmp/tmptqOmCq/5S9s02wc9x/fdroidserver-1.0.0/locale/de/LC_MESSAGES/fdroidserver.mo differ Binary files /tmp/tmptqOmCq/As4wx2_aTZ/fdroidserver-0.9.1/locale/es/LC_MESSAGES/fdroidserver.mo and /tmp/tmptqOmCq/5S9s02wc9x/fdroidserver-1.0.0/locale/es/LC_MESSAGES/fdroidserver.mo differ Binary files /tmp/tmptqOmCq/As4wx2_aTZ/fdroidserver-0.9.1/locale/tr/LC_MESSAGES/fdroidserver.mo and /tmp/tmptqOmCq/5S9s02wc9x/fdroidserver-1.0.0/locale/tr/LC_MESSAGES/fdroidserver.mo differ Binary files /tmp/tmptqOmCq/As4wx2_aTZ/fdroidserver-0.9.1/locale/zh_Hant/LC_MESSAGES/fdroidserver.mo and /tmp/tmptqOmCq/5S9s02wc9x/fdroidserver-1.0.0/locale/zh_Hant/LC_MESSAGES/fdroidserver.mo differ diff -Nru fdroidserver-0.9.1/makebuildserver fdroidserver-1.0.0/makebuildserver --- fdroidserver-0.9.1/makebuildserver 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/makebuildserver 2017-12-28 21:49:56.000000000 +0000 @@ -178,8 +178,6 @@ '9b742d34590fe73fb7229e34835ecffb1846ca389d9f924f0b2a37de525dc6b8'), ('https://dl.google.com/android/repository/platform-26_r02.zip', '2aafa7d19c5e9c4b643ee6ade3d85ef89dc2f79e8383efdb9baf7fddad74b52a'), - ('https://dl.google.com/android/repository/platform-27_r01.zip', - 'cbba6f8fcf025e1b533326746763aa1d6e2cf4001b1b441602bb44d253bc49ac'), ('https://dl.google.com/android/repository/build-tools_r17-linux.zip', '4c8444972343a19045236f6924bd7f12046287c70dace96ab88b2159c8ec0e74'), ('https://dl.google.com/android/repository/build-tools_r18.0.1-linux.zip', @@ -252,6 +250,8 @@ '53d3322774a0bf229b372c0288108b4bfa27d74725fce8f0a3393e8df6b9ef22'), ('https://dl.google.com/android/repository/build-tools_r27.0.1-linux.zip', '2e8e0946e93af50667ae02ef200e81c1ac2269b59f14955397245e9e441e8b1e'), + ('https://dl.google.com/android/repository/build-tools_r27.0.2-linux.zip', + 'e73674e065a93ffb05c30a15c8021c0d72ea7c3c206eb9020eb93e49e42ce851'), # the binaries that Google uses are here: # https://android.googlesource.com/platform/tools/external/gradle/+/studio-1.5/ ('https://services.gradle.org/distributions/gradle-1.4-bin.zip', @@ -336,6 +336,8 @@ '8dcbf44eef92575b475dcb1ce12b5f19d38dc79e84c662670248dc8b8247654c'), ('https://downloads.gradle.org/distributions/gradle-4.3.1-bin.zip', '15ebe098ce0392a2d06d252bff24143cc88c4e963346582c8d88814758d93ac7'), + ('https://downloads.gradle.org/distributions/gradle-4.4-bin.zip', + 'fa4873ae2c7f5e8c02ec6948ba95848cedced6134772a0169718eadcb39e0a2f'), ('https://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin', '102d6723f67ff1384330d12c45854315d6452d6510286f4e5891e00a5a8f1d5a'), ('https://dl.google.com/android/ndk/android-ndk-r9b-linux-x86_64.tar.bz2', @@ -553,10 +555,16 @@ for d in ('.m2', '.gradle/caches', '.gradle/wrapper', '.pip_download_cache'): fullpath = os.path.join(os.getenv('HOME'), d) if os.path.isdir(fullpath): - # TODO newer versions of vagrant provide `vagrant rsync` + ssh_command = ' '.join(('ssh -i {0} -p {1}'.format(key, port), + '-o StrictHostKeyChecking=no', + '-o UserKnownHostsFile=/dev/null', + '-o LogLevel=FATAL', + '-o IdentitiesOnly=yes', + '-o PasswordAuthentication=no')) + # TODO vagrant 1.5+ provides `vagrant rsync` run_via_vagrant_ssh(v, ['cd ~ && test -d', d, '|| mkdir -p', d]) - subprocess.call(['rsync', '-axv', '--progress', '--delete', '-e', - 'ssh -i {0} -p {1} -oIdentitiesOnly=yes'.format(key, port), + subprocess.call(['rsync', '-ax', '--delete', '-e', + ssh_command, fullpath + '/', user + '@' + hostname + ':~/' + d + '/']) diff -Nru fdroidserver-0.9.1/PKG-INFO fdroidserver-1.0.0/PKG-INFO --- fdroidserver-0.9.1/PKG-INFO 2017-11-27 19:09:53.000000000 +0000 +++ fdroidserver-1.0.0/PKG-INFO 2018-01-03 20:42:33.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: fdroidserver -Version: 0.9.1 +Version: 1.0.0 Summary: F-Droid Server Tools Home-page: https://f-droid.org Author: The F-Droid Project @@ -55,6 +55,55 @@ 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 `__ 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 `__ + and push this to a new branch of your fork. + + Alternatively `run them + locally `__ + 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 ~~~~~~~~~~~~~~ diff -Nru fdroidserver-0.9.1/README.rst fdroidserver-1.0.0/README.rst --- fdroidserver-0.9.1/README.rst 2017-11-27 19:09:53.000000000 +0000 +++ fdroidserver-1.0.0/README.rst 2018-01-03 20:42:26.000000000 +0000 @@ -47,6 +47,55 @@ 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 `__ 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 `__ +and push this to a new branch of your fork. + +Alternatively `run them +locally `__ +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 ~~~~~~~~~~~~~~ diff -Nru fdroidserver-0.9.1/setup.cfg fdroidserver-1.0.0/setup.cfg --- fdroidserver-0.9.1/setup.cfg 2017-11-27 19:09:53.000000000 +0000 +++ fdroidserver-1.0.0/setup.cfg 2018-01-03 20:42:33.000000000 +0000 @@ -1,5 +1,5 @@ [aliases] -release = versioncheck register compile_catalog sdist upload --sign +release = versioncheck compile_catalog register sdist upload --sign [extract_messages] keywords = _ @@ -23,7 +23,7 @@ directory = locale [egg_info] -tag_build = tag_date = 0 tag_svn_revision = 0 +tag_build = diff -Nru fdroidserver-0.9.1/setup.py fdroidserver-1.0.0/setup.py --- fdroidserver-0.9.1/setup.py 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/setup.py 2018-01-03 20:40:59.000000000 +0000 @@ -66,7 +66,7 @@ readme = '' setup(name='fdroidserver', - version='0.9.1', + version='1.0.0', description='F-Droid Server Tools', long_description=readme, author='The F-Droid Project', @@ -74,10 +74,13 @@ url='https://f-droid.org', license='AGPL-3.0', packages=['fdroidserver', 'fdroidserver.asynchronousfilereader'], - scripts=['fdroid', 'fd-commit', 'makebuildserver'], + scripts=['fdroid', 'makebuildserver'], data_files=get_data_files(), python_requires='>=3.4', cmdclass={'versioncheck': VersionCheckCommand}, + setup_requires=[ + 'babel', + ], install_requires=[ 'clint', 'GitPython', @@ -89,6 +92,7 @@ 'pyasn1-modules', 'python-vagrant', 'PyYAML', + 'qrcode', 'ruamel.yaml >= 0.13', 'requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0', 'docker-py >= 1.9, < 2.0', diff -Nru fdroidserver-0.9.1/tests/build.TestCase fdroidserver-1.0.0/tests/build.TestCase --- fdroidserver-0.9.1/tests/build.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/build.TestCase 2017-12-02 12:33:56.000000000 +0000 @@ -3,6 +3,7 @@ # http://www.drdobbs.com/testing/unit-testing-with-python/240165163 import inspect +import logging import optparse import os import re @@ -46,22 +47,27 @@ self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.isfile(path)) + def setUp(self): + logging.basicConfig(level=logging.DEBUG) + self.basedir = os.path.join(localmodule, 'tests') + self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles')) + if not os.path.exists(self.tmpdir): + os.makedirs(self.tmpdir) + os.chdir(self.basedir) + def test_force_gradle_build_tools(self): - testsbase = os.path.join(os.path.dirname(__file__), '..', '.testfiles') - if not os.path.exists(testsbase): - os.makedirs(testsbase) - testsdir = tempfile.mkdtemp(prefix='test_adapt_gradle', dir=testsbase) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) shutil.copytree(os.path.join(os.path.dirname(__file__), 'source-files'), - os.path.join(testsdir, 'source-files')) + os.path.join(testdir, 'source-files')) teststring = 'FAKE_VERSION_FOR_TESTING' - fdroidserver.build.force_gradle_build_tools(testsdir, teststring) + fdroidserver.build.force_gradle_build_tools(testdir, teststring) pattern = re.compile(bytes("buildToolsVersion[\s=]+'%s'\s+" % teststring, 'utf8')) for p in ('source-files/fdroid/fdroidclient/build.gradle', 'source-files/Zillode/syncthing-silk/build.gradle', 'source-files/open-keychain/open-keychain/build.gradle', 'source-files/osmandapp/osmand/build.gradle', 'source-files/open-keychain/open-keychain/OpenKeychain/build.gradle'): - with open(os.path.join(testsdir, p), 'rb') as f: + with open(os.path.join(testdir, p), 'rb') as f: filedata = f.read() self.assertIsNotNone(pattern.search(filedata)) diff -Nru fdroidserver-0.9.1/tests/common.TestCase fdroidserver-1.0.0/tests/common.TestCase --- fdroidserver-0.9.1/tests/common.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/common.TestCase 2017-12-28 22:07:26.000000000 +0000 @@ -22,6 +22,7 @@ if localmodule not in sys.path: sys.path.insert(0, localmodule) +import fdroidserver.index import fdroidserver.signindex import fdroidserver.common import fdroidserver.metadata @@ -86,18 +87,15 @@ sdk_path = os.getenv('ANDROID_HOME') if os.path.exists(sdk_path): fdroidserver.common.config['sdk_path'] = sdk_path - if os.path.exists('/usr/bin/aapt'): - # this test only works when /usr/bin/aapt is installed - self._find_all() build_tools = os.path.join(sdk_path, 'build-tools') - if self._set_build_tools(): + if self._set_build_tools() or os.path.exists('/usr/bin/aapt'): self._find_all() else: print('no build-tools found: ' + build_tools) def test_find_java_root_path(self): - tmptestsdir = tempfile.mkdtemp(prefix='test_find_java_root_path', dir=self.tmpdir) - os.chdir(tmptestsdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) + os.chdir(testdir) all_pathlists = [ ([ # Debian @@ -170,11 +168,11 @@ testint = 99999999 teststr = 'FAKE_STR_FOR_TESTING' - tmptestsdir = tempfile.mkdtemp(prefix='test_prepare_sources', dir=self.tmpdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) shutil.copytree(os.path.join(self.basedir, 'source-files'), - os.path.join(tmptestsdir, 'source-files')) + os.path.join(testdir, 'source-files')) - testdir = os.path.join(tmptestsdir, 'source-files', 'fdroid', 'fdroidclient') + fdroidclient_testdir = os.path.join(testdir, 'source-files', 'fdroid', 'fdroidclient') config = dict() config['sdk_path'] = os.getenv('ANDROID_HOME') @@ -201,13 +199,14 @@ def getsrclib(self): return None - fdroidserver.common.prepare_source(FakeVcs(), app, build, testdir, testdir, testdir) + fdroidserver.common.prepare_source(FakeVcs(), app, build, + fdroidclient_testdir, fdroidclient_testdir, fdroidclient_testdir) - with open(os.path.join(testdir, 'build.gradle'), 'r') as f: + with open(os.path.join(fdroidclient_testdir, 'build.gradle'), 'r') as f: filedata = f.read() self.assertIsNotNone(re.search("\s+compileSdkVersion %s\s+" % testint, filedata)) - with open(os.path.join(testdir, 'AndroidManifest.xml')) as f: + with open(os.path.join(fdroidclient_testdir, 'AndroidManifest.xml')) as f: filedata = f.read() self.assertIsNone(re.search('android:debuggable', filedata)) self.assertIsNotNone(re.search('android:versionName="%s"' % build.versionName, filedata)) @@ -215,7 +214,7 @@ def test_prepare_sources_refresh(self): packageName = 'org.fdroid.ci.test.app' - testdir = tempfile.mkdtemp(prefix='test_prepare_sources_refresh', dir=self.tmpdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) print('testdir', testdir) os.chdir(testdir) os.mkdir('build') @@ -262,7 +261,7 @@ fdroidserver.signindex.config = config sourcedir = os.path.join(self.basedir, 'signindex') - testsdir = tempfile.mkdtemp(prefix='test_signjar', dir=self.tmpdir) + testsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) for f in ('testy.jar', 'guardianproject.jar',): sourcefile = os.path.join(sourcedir, f) testfile = os.path.join(testsdir, f) @@ -277,12 +276,56 @@ config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') fdroidserver.common.config = config + self.assertTrue(fdroidserver.common.verify_apk_signature('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')) + self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_1.apk')) + self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_2.apk')) + self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_3.apk')) + self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_4.apk')) + self.assertTrue(fdroidserver.common.verify_apk_signature('org.dyndns.fules.ck_20.apk')) self.assertTrue(fdroidserver.common.verify_apk_signature('urzip.apk')) self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badcert.apk')) self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badsig.apk')) self.assertTrue(fdroidserver.common.verify_apk_signature('urzip-release.apk')) self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-release-unsigned.apk')) + def test_verify_old_apk_signature(self): + fdroidserver.common.config = None + config = fdroidserver.common.read_config(fdroidserver.common.options) + config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') + fdroidserver.common.config = config + + self.assertTrue(fdroidserver.common.verify_old_apk_signature('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_1.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_2.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_3.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_4.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.dyndns.fules.ck_20.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('urzip.apk')) + self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-badcert.apk')) + self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-badsig.apk')) + self.assertTrue(fdroidserver.common.verify_old_apk_signature('urzip-release.apk')) + self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-release-unsigned.apk')) + + def test_verify_jar_signature_succeeds(self): + fdroidserver.common.config = None + config = fdroidserver.common.read_config(fdroidserver.common.options) + config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') + fdroidserver.common.config = config + source_dir = os.path.join(self.basedir, 'signindex') + for f in ('testy.jar', 'guardianproject.jar'): + testfile = os.path.join(source_dir, f) + fdroidserver.common.verify_jar_signature(testfile) + + def test_verify_jar_signature_fails(self): + fdroidserver.common.config = None + config = fdroidserver.common.read_config(fdroidserver.common.options) + config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') + fdroidserver.common.config = config + source_dir = os.path.join(self.basedir, 'signindex') + testfile = os.path.join(source_dir, 'unsigned.jar') + with self.assertRaises(fdroidserver.index.VerificationException): + fdroidserver.common.verify_jar_signature(testfile) + def test_verify_apks(self): fdroidserver.common.config = None config = fdroidserver.common.read_config(fdroidserver.common.options) @@ -291,7 +334,7 @@ sourceapk = os.path.join(self.basedir, 'urzip.apk') - testdir = tempfile.mkdtemp(prefix='test_verify_apks', dir=self.tmpdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) print('testdir', testdir) copyapk = os.path.join(testdir, 'urzip-copy.apk') @@ -455,6 +498,41 @@ self.assertEqual(keytoolcertfingerprint, fdroidserver.common.apk_signer_fingerprint_short(apkfile)) + def test_sign_apk(self): + fdroidserver.common.config = None + config = fdroidserver.common.read_config(fdroidserver.common.options) + config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') + config['keyalias'] = 'sova' + config['keystorepass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI=' + config['keypass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI=' + config['keystore'] = os.path.join(self.basedir, 'keystore.jks') + fdroidserver.common.config = config + fdroidserver.signindex.config = config + + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) + unsigned = os.path.join(testdir, 'urzip-release-unsigned.apk') + signed = os.path.join(testdir, 'urzip-release.apk') + + self.assertFalse(fdroidserver.common.verify_apk_signature(unsigned)) + + shutil.copy(os.path.join(self.basedir, 'urzip-release-unsigned.apk'), testdir) + fdroidserver.common.sign_apk(unsigned, signed, config['keyalias']) + self.assertTrue(os.path.isfile(signed)) + self.assertFalse(os.path.isfile(unsigned)) + self.assertTrue(fdroidserver.common.verify_apk_signature(signed)) + + # now sign an APK with minSdkVersion >= 18 + unsigned = os.path.join(testdir, 'duplicate.permisssions_9999999-unsigned.apk') + signed = os.path.join(testdir, 'duplicate.permisssions_9999999.apk') + shutil.copy(os.path.join(self.basedir, 'repo', 'duplicate.permisssions_9999999.apk'), + os.path.join(unsigned)) + fdroidserver.common.apk_strip_signatures(unsigned, strip_manifest=True) + fdroidserver.common.sign_apk(unsigned, signed, config['keyalias']) + self.assertTrue(os.path.isfile(signed)) + self.assertFalse(os.path.isfile(unsigned)) + self.assertTrue(fdroidserver.common.verify_apk_signature(signed)) + self.assertEqual(18, fdroidserver.common.get_minSdkVersion_aapt(signed)) + def test_get_api_id_aapt(self): config = dict() @@ -471,6 +549,61 @@ with self.assertRaises(FDroidException): fdroidserver.common.get_apk_id_aapt('nope') + def test_get_minSdkVersion_aapt(self): + + config = dict() + fdroidserver.common.fill_config_defaults(config) + fdroidserver.common.config = config + self._set_build_tools() + config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt') + + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('bad-unicode-πÇÇ现代通用字-български-عربي1.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_1.apk') + self.assertEqual(14, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_2.apk') + self.assertEqual(14, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_3.apk') + self.assertEqual(14, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_4.apk') + self.assertEqual(14, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.dyndns.fules.ck_20.apk') + self.assertEqual(7, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-badcert.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-badsig.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-release.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-release-unsigned.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_3.apk') + self.assertEqual(3, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_4.apk') + self.assertEqual(3, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_5.apk') + self.assertEqual(3, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_6.apk') + self.assertEqual(14, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.oldversion_1444412523.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.mainpatch.current_1619_another-release-key.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.mainpatch.current_1619.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101613.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101615.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101617.apk') + self.assertEqual(4, minSdkVersion) + minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/urzip-; Рахма́нинов, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢尔盖·.apk') + + with self.assertRaises(FDroidException): + fdroidserver.common.get_minSdkVersion_aapt('nope') + def test_apk_release_name(self): appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('com.serwylo.lexica_905.apk') self.assertEqual(appid, 'com.serwylo.lexica') @@ -491,6 +624,88 @@ sig = fdroidserver.common.metadata_find_developer_signature('org.smssecure.smssecure') self.assertEqual('b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868', sig) + def test_parse_androidmanifests(self): + source_files_dir = os.path.join(os.path.dirname(__file__), 'source-files') + app = fdroidserver.metadata.App() + app.id = 'org.fdroid.fdroid' + paths = [ + os.path.join(source_files_dir, 'fdroid', 'fdroidclient', 'AndroidManifest.xml'), + os.path.join(source_files_dir, 'fdroid', 'fdroidclient', 'build.gradle'), + ] + for path in paths: + self.assertTrue(os.path.isfile(path)) + self.assertEqual(('0.94-test', '940', 'org.fdroid.fdroid'), + fdroidserver.common.parse_androidmanifests(paths, app)) + + def test_parse_androidmanifests_with_flavor(self): + source_files_dir = os.path.join(os.path.dirname(__file__), 'source-files') + + app = fdroidserver.metadata.App() + build = fdroidserver.metadata.Build() + build.gradle = ['devVersion'] + app.builds = [build] + app.id = 'org.fdroid.fdroid.dev' + paths = [ + os.path.join(source_files_dir, 'fdroid', 'fdroidclient', 'AndroidManifest.xml'), + os.path.join(source_files_dir, 'fdroid', 'fdroidclient', 'build.gradle'), + ] + for path in paths: + self.assertTrue(os.path.isfile(path)) + self.assertEqual(('0.95-dev', '949', 'org.fdroid.fdroid.dev'), + fdroidserver.common.parse_androidmanifests(paths, app)) + + app = fdroidserver.metadata.App() + build = fdroidserver.metadata.Build() + build.gradle = ['free'] + app.builds = [build] + app.id = 'eu.siacs.conversations' + paths = [ + os.path.join(source_files_dir, 'eu.siacs.conversations', 'build.gradle'), + ] + for path in paths: + self.assertTrue(os.path.isfile(path)) + self.assertEqual(('1.23.1', '245', 'eu.siacs.conversations'), + fdroidserver.common.parse_androidmanifests(paths, app)) + + app = fdroidserver.metadata.App() + build = fdroidserver.metadata.Build() + build.gradle = ['generic'] + app.builds = [build] + app.id = 'com.nextcloud.client' + paths = [ + os.path.join(source_files_dir, 'com.nextcloud.client', 'build.gradle'), + ] + for path in paths: + self.assertTrue(os.path.isfile(path)) + self.assertEqual(('2.0.0', '20000099', 'com.nextcloud.client'), + fdroidserver.common.parse_androidmanifests(paths, app)) + + app = fdroidserver.metadata.App() + build = fdroidserver.metadata.Build() + build.gradle = ['versionDev'] + app.builds = [build] + app.id = 'com.nextcloud.android.beta' + paths = [ + os.path.join(source_files_dir, 'com.nextcloud.client', 'build.gradle'), + ] + for path in paths: + self.assertTrue(os.path.isfile(path)) + self.assertEqual(('20171223', '20171223', 'com.nextcloud.android.beta'), + fdroidserver.common.parse_androidmanifests(paths, app)) + + app = fdroidserver.metadata.App() + build = fdroidserver.metadata.Build() + build.gradle = ['standard'] + app.builds = [build] + app.id = 'at.bitfire.davdroid' + paths = [ + os.path.join(source_files_dir, 'at.bitfire.davdroid', 'build.gradle'), + ] + for path in paths: + self.assertTrue(os.path.isfile(path)) + self.assertEqual(('1.9.8.1-ose', '197', 'at.bitfire.davdroid'), + fdroidserver.common.parse_androidmanifests(paths, app)) + if __name__ == "__main__": parser = optparse.OptionParser() @@ -500,4 +715,4 @@ newSuite = unittest.TestSuite() newSuite.addTest(unittest.makeSuite(CommonTest)) - unittest.main(failfast=True) + unittest.main(failfast=False) diff -Nru fdroidserver-0.9.1/tests/complete-ci-tests fdroidserver-1.0.0/tests/complete-ci-tests --- fdroidserver-0.9.1/tests/complete-ci-tests 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/complete-ci-tests 2017-12-20 22:46:37.000000000 +0000 @@ -75,28 +75,6 @@ #------------------------------------------------------------------------------# -# test building the source tarball, then installing it -cd $WORKSPACE -python3 setup.py compile_catalog sdist - -# make sure translation files got compiled and included -tar tzf dist/fdroidserver-*.tar.gz | grep locale/de/LC_MESSAGES/fdroidserver.mo - -rm -rf $WORKSPACE/env -$pyvenv $WORKSPACE/env -. $WORKSPACE/env/bin/activate -# workaround https://github.com/pypa/setuptools/issues/937 -pip3 install --quiet setuptools==33.1.1 -pip3 install --quiet dist/fdroidserver-*.tar.gz - -# make sure translation files were installed -test -e $WORKSPACE/env/share/locale/de/LC_MESSAGES/fdroidserver.mo - -# run tests in new pip+pyvenv install -fdroid=$WORKSPACE/env/bin/fdroid $WORKSPACE/tests/run-tests $apksource - - -#------------------------------------------------------------------------------# # test install using install direct from git repo cd $WORKSPACE rm -rf $WORKSPACE/env @@ -115,11 +93,6 @@ #------------------------------------------------------------------------------# -# run git pre-commit hook for pep8, pyflakes, etc -sh hooks/pre-commit - - -#------------------------------------------------------------------------------# # run pylint # only run it where it will work, for example, the pyvenvs above don't have pylint diff -Nru fdroidserver-0.9.1/tests/import.TestCase fdroidserver-1.0.0/tests/import.TestCase --- fdroidserver-0.9.1/tests/import.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/import.TestCase 2017-12-02 12:33:56.000000000 +0000 @@ -3,6 +3,7 @@ # http://www.drdobbs.com/testing/unit-testing-with-python/240165163 import inspect +import logging import optparse import os import requests @@ -24,8 +25,15 @@ class ImportTest(unittest.TestCase): '''fdroid import''' + def setUp(self): + logging.basicConfig(level=logging.DEBUG) + self.basedir = os.path.join(localmodule, 'tests') + self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles')) + if not os.path.exists(self.tmpdir): + os.makedirs(self.tmpdir) + os.chdir(self.basedir) + def test_import_gitlab(self): - os.chdir(os.path.dirname(__file__)) # FDroidPopen needs some config to work config = dict() fdroidserver.common.fill_config_defaults(config) diff -Nru fdroidserver-0.9.1/tests/index.TestCase fdroidserver-1.0.0/tests/index.TestCase --- fdroidserver-0.9.1/tests/index.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/index.TestCase 2017-12-14 15:54:01.000000000 +0000 @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import inspect +import logging import optparse import os import sys @@ -31,30 +32,21 @@ class IndexTest(unittest.TestCase): def setUp(self): + logging.basicConfig(level=logging.DEBUG) + self.basedir = os.path.join(localmodule, 'tests') + self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles')) + if not os.path.exists(self.tmpdir): + os.makedirs(self.tmpdir) + os.chdir(self.basedir) + fdroidserver.common.config = None config = fdroidserver.common.read_config(fdroidserver.common.options) config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner') fdroidserver.common.config = config fdroidserver.signindex.config = config - @staticmethod - def test_verify_jar_signature_succeeds(): - basedir = os.path.dirname(__file__) - source_dir = os.path.join(basedir, 'signindex') - for f in ('testy.jar', 'guardianproject.jar'): - testfile = os.path.join(source_dir, f) - fdroidserver.common.verify_jar_signature(testfile) - - def test_verify_jar_signature_fails(self): - basedir = os.path.dirname(__file__) - source_dir = os.path.join(basedir, 'signindex') - testfile = os.path.join(source_dir, 'unsigned.jar') - with self.assertRaises(fdroidserver.index.VerificationException): - fdroidserver.common.verify_jar_signature(testfile) - def test_get_public_key_from_jar_succeeds(self): - basedir = os.path.dirname(__file__) - source_dir = os.path.join(basedir, 'signindex') + source_dir = os.path.join(self.basedir, 'signindex') for f in ('testy.jar', 'guardianproject.jar'): testfile = os.path.join(source_dir, f) jar = zipfile.ZipFile(testfile) @@ -68,8 +60,7 @@ self.assertTrue(fingerprint == GP_FINGERPRINT) def test_get_public_key_from_jar_fails(self): - basedir = os.path.dirname(__file__) - source_dir = os.path.join(basedir, 'signindex') + source_dir = os.path.join(self.basedir, 'signindex') testfile = os.path.join(source_dir, 'unsigned.jar') jar = zipfile.ZipFile(testfile) with self.assertRaises(fdroidserver.index.VerificationException): @@ -226,9 +217,6 @@ if __name__ == "__main__": - if os.path.basename(os.getcwd()) != 'tests' and os.path.isdir('tests'): - os.chdir('tests') - parser = optparse.OptionParser() parser.add_option("-v", "--verbose", action="store_true", default=False, help="Spew out even more information than normal") diff -Nru fdroidserver-0.9.1/tests/lint.TestCase fdroidserver-1.0.0/tests/lint.TestCase --- fdroidserver-0.9.1/tests/lint.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/lint.TestCase 2017-12-02 12:33:56.000000000 +0000 @@ -32,8 +32,7 @@ self.assertTrue(fdroidserver.lint.check_for_unsupported_metadata_files()) tmpdir = os.path.join(localmodule, '.testfiles') - tmptestsdir = tempfile.mkdtemp(prefix='test_check_for_unsupported_metadata_files-', - dir=tmpdir) + tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=tmpdir) self.assertFalse(fdroidserver.lint.check_for_unsupported_metadata_files(tmptestsdir + '/')) shutil.copytree(os.path.join(localmodule, 'tests', 'metadata'), os.path.join(tmptestsdir, 'metadata'), diff -Nru fdroidserver-0.9.1/tests/metadata/dump/com.politedroid.yaml fdroidserver-1.0.0/tests/metadata/dump/com.politedroid.yaml --- fdroidserver-0.9.1/tests/metadata/dump/com.politedroid.yaml 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/metadata/dump/com.politedroid.yaml 2017-12-14 15:54:01.000000000 +0000 @@ -17,6 +17,7 @@ Donate: null FlattrID: null IssueTracker: https://github.com/miguelvps/PoliteDroid/issues +LiberapayID: null License: GPL-3.0 Litecoin: null MaintainerNotes: '' diff -Nru fdroidserver-0.9.1/tests/metadata/dump/org.adaway.yaml fdroidserver-1.0.0/tests/metadata/dump/org.adaway.yaml --- fdroidserver-0.9.1/tests/metadata/dump/org.adaway.yaml 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/metadata/dump/org.adaway.yaml 2017-12-14 15:54:01.000000000 +0000 @@ -40,6 +40,7 @@ Donate: http://sufficientlysecure.org/index.php/adaway FlattrID: '369138' IssueTracker: https://github.com/dschuermann/ad-away/issues +LiberapayID: null License: GPL-3.0 Litecoin: null MaintainerNotes: '' diff -Nru fdroidserver-0.9.1/tests/metadata/dump/org.smssecure.smssecure.yaml fdroidserver-1.0.0/tests/metadata/dump/org.smssecure.smssecure.yaml --- fdroidserver-0.9.1/tests/metadata/dump/org.smssecure.smssecure.yaml 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/metadata/dump/org.smssecure.smssecure.yaml 2017-12-14 15:54:01.000000000 +0000 @@ -37,6 +37,7 @@ Donate: null FlattrID: null IssueTracker: https://github.com/SMSSecure/SMSSecure/issues +LiberapayID: null License: GPL-3.0 Litecoin: null MaintainerNotes: '' diff -Nru fdroidserver-0.9.1/tests/metadata/dump/org.videolan.vlc.yaml fdroidserver-1.0.0/tests/metadata/dump/org.videolan.vlc.yaml --- fdroidserver-0.9.1/tests/metadata/dump/org.videolan.vlc.yaml 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/metadata/dump/org.videolan.vlc.yaml 2017-12-14 15:54:01.000000000 +0000 @@ -24,6 +24,7 @@ Donate: http://www.videolan.org/contribute.html#money FlattrID: null IssueTracker: http://www.videolan.org/support/index.html#bugs +LiberapayID: null License: GPL-3.0 Litecoin: null MaintainerNotes: 'Instructions and dependencies here: http://wiki.videolan.org/AndroidCompile diff -Nru fdroidserver-0.9.1/tests/metadata.TestCase fdroidserver-1.0.0/tests/metadata.TestCase --- fdroidserver-0.9.1/tests/metadata.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/metadata.TestCase 2017-12-02 12:33:56.000000000 +0000 @@ -2,9 +2,13 @@ # http://www.drdobbs.com/testing/unit-testing-with-python/240165163 +import glob import inspect +import logging import optparse import os +import random +import shutil import sys import unittest import yaml @@ -23,15 +27,20 @@ class MetadataTest(unittest.TestCase): '''fdroidserver/metadata.py''' + def setUp(self): + logging.basicConfig(level=logging.DEBUG) + self.basedir = os.path.join(localmodule, 'tests') + self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles')) + if not os.path.exists(self.tmpdir): + os.makedirs(self.tmpdir) + os.chdir(self.basedir) + def test_read_metadata(self): def _build_yaml_representer(dumper, data): '''Creates a YAML representation of a Build instance''' return dumper.represent_dict(data) - testsdir = os.path.dirname(__file__) - os.chdir(testsdir) - self.maxDiff = None # these need to be set to prevent code running on None, only @@ -58,12 +67,7 @@ # yaml.dump(frommeta, f, default_flow_style=False) def test_rewrite_yaml_fakeotaupdate(self): - - # setup/reset test dir if necessary and setup params - tmpdir = os.path.join(os.path.dirname(__file__), '..', '.testfiles') - if not os.path.exists(tmpdir): - os.makedirs(tmpdir) - testdir = tempfile.mkdtemp(prefix='test_rewrite_metadata_', dir=tmpdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) fdroidserver.common.config = {'accepted_formats': ['txt', 'yml']} # rewrite metadata @@ -79,12 +83,7 @@ self.assertEqual(result.read(), orig.read()) def test_rewrite_yaml_fdroidclient(self): - - # setup/reset test dir if necessary and setup params - tmpdir = os.path.join(os.path.dirname(__file__), '..', '.testfiles') - if not os.path.exists(tmpdir): - os.makedirs(tmpdir) - testdir = tempfile.mkdtemp(prefix='test_rewrite_metadata_', dir=tmpdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) fdroidserver.common.config = {'accepted_formats': ['txt', 'yml']} # rewrite metadata @@ -100,12 +99,7 @@ self.assertEqual(result.read(), orig.read()) def test_rewrite_yaml_special_build_params(self): - - # setup/reset test dir if necessary and setup params - tmpdir = os.path.join(os.path.dirname(__file__), '..', '.testfiles') - if not os.path.exists(tmpdir): - os.makedirs(tmpdir) - testdir = tempfile.mkdtemp(prefix='test_rewrite_metadata_', dir=tmpdir) + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) fdroidserver.common.config = {'accepted_formats': ['txt', 'yml']} # rewrite metadata @@ -120,6 +114,31 @@ self.maxDiff = None self.assertEqual(result.read(), orig.read()) + def test_read_metadata_sort_by_time(self): + testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir) + metadatadir = os.path.join(testdir, 'metadata') + os.makedirs(metadatadir) + fdroidserver.common.config = {'accepted_formats': ['txt']} + + randomlist = [] + randomapps = glob.glob(os.path.join(self.basedir, 'metadata', '*.txt')) + random.shuffle(randomapps) + i = 1 + for f in randomapps: + shutil.copy(f, metadatadir) + new = os.path.join(metadatadir, os.path.basename(f)) + stat = os.stat(new) + os.utime(new, (stat.st_ctime, stat.st_mtime + i)) + # prepend new item so newest is always first + randomlist = [os.path.basename(f)[:-4]] + randomlist + i += 1 + os.chdir(testdir) + allapps = fdroidserver.metadata.read_metadata(xref=True, sort_by_time=True) + allappids = [] + for appid, app in allapps.items(): + allappids.append(appid) + self.assertEqual(randomlist, allappids) + if __name__ == "__main__": parser = optparse.OptionParser() diff -Nru fdroidserver-0.9.1/tests/repo/index.xml fdroidserver-1.0.0/tests/repo/index.xml --- fdroidserver-0.9.1/tests/repo/index.xml 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/repo/index.xml 2017-12-29 14:52:13.000000000 +0000 @@ -8,6 +8,38 @@ + + duplicate.permisssions + 2017-12-22 + 2017-12-22 + Duplicate Permisssions + Test app for all possible <uses-permissions> + duplicate.permisssions.9999999.png + <p>No description available</p> + Unknown + tests + tests + + + + + 9999999 + + 0.3-7-gb817ac8 + 9999999 + duplicate.permisssions_9999999.apk + 9ffc7e9b2740ce664059194805b2fbfc08b7970c8448a22b8bd828dfd6ad161c + 11988 + 18 + 27 + 2017-12-22 + 2d337e40aef77564bf62781ac424595c + ACCESS_NETWORK_STATE,ACCESS_WIFI_STATE,CHANGE_WIFI_MULTICAST_STATE,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE + + + + + fake.ota.update 2016-03-10 diff -Nru fdroidserver-0.9.1/tests/run-tests fdroidserver-1.0.0/tests/run-tests --- fdroidserver-0.9.1/tests/run-tests 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/run-tests 2017-12-29 15:04:57.000000000 +0000 @@ -9,7 +9,7 @@ copy_apks_into_repo() { set +x find $APKDIR -type f -name '*.apk' -print0 | while IFS= read -r -d '' f; do - echo $f | grep -F -v -e unaligned -e unsigned -e badsig -e badcert -e bad-unicode || continue + echo $f | grep -F -v -e unaligned -e unsigned -e badsig -e badcert -e bad-unicode -e janus.apk || continue apk=`$aapt dump badging "$f" | sed -n "s,^package: name='\(.*\)' versionCode='\([0-9][0-9]*\)' .*,\1_\2.apk,p"` test "$f" -nt repo/$apk && rm -f repo/$apk # delete existing if $f is newer if [ ! -e repo/$apk ] && [ ! -e archive/$apk ]; then @@ -41,6 +41,17 @@ TMPDIR=$WORKSPACE/.testfiles mktemp } +fdroid_init_with_prebuilt_keystore() { + if [ -z "$1" ]; then + keystore=$WORKSPACE/tests/keystore.jks + else + keystore="$1" + fi + $fdroid init --keystore $keystore --repo-keyalias=sova + echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py + echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +} + # the < is reverse since 0 means success in exit codes have_git_2_3() { python3 -c "import sys; from distutils.version import LooseVersion as V; sys.exit(V(sys.argv[3]) < V('2.3'))" `git --version` @@ -91,11 +102,6 @@ aapt=`ls -1 $ANDROID_HOME/build-tools/*/aapt | sort | tail -1` fi -# allow the location of python to be overridden -if [ -z $python ]; then - python=python3 -fi - # try to use GNU sed on OSX/BSD cuz BSD sed sucks if which gsed; then sed=gsed @@ -165,7 +171,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -$fdroid init +fdroid_init_with_prebuilt_keystore $sed -i.tmp 's,^ *repo_description.*,repo_description = """获取已安装在您的设备上的应用的,' config.py echo "mirrors = ('https://foo.bar/fdroid', 'http://secret.onion/fdroid')" >> config.py mkdir metadata @@ -219,11 +225,8 @@ REPOROOT=`create_test_dir` GNUPGHOME=$REPOROOT/gnupghome -KEYSTORE=$WORKSPACE/tests/keystore.jks cd $REPOROOT -$fdroid init --keystore $KEYSTORE --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore cp -a $WORKSPACE/tests/metadata $WORKSPACE/tests/repo $WORKSPACE/tests/stats $REPOROOT/ cp -a $WORKSPACE/tests/gnupghome $GNUPGHOME chmod 0700 $GNUPGHOME @@ -263,10 +266,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -cp $WORKSPACE/tests/keystore.jks $REPOROOT/ -$fdroid init --keystore keystore.jks --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore echo "accepted_formats = ['txt']" >> config.py $sed -i.tmp '/allow_disabled_algorithms/d' config.py test -d metadata || mkdir metadata @@ -293,10 +293,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -cp $WORKSPACE/tests/keystore.jks $REPOROOT/ -$fdroid init --keystore keystore.jks --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore echo "accepted_formats = ['txt']" >> config.py test -d metadata || mkdir metadata cp $WORKSPACE/tests/metadata/com.politedroid.txt metadata/ @@ -367,10 +364,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -cp $WORKSPACE/tests/keystore.jks $REPOROOT/ -$fdroid init --keystore keystore.jks --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore echo "accepted_formats = ['txt']" >> config.py test -d metadata || mkdir metadata cp $WORKSPACE/tests/metadata/com.politedroid.txt metadata/ @@ -436,10 +430,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -cp $WORKSPACE/tests/keystore.jks $REPOROOT/ -$fdroid init --keystore keystore.jks --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore echo "accepted_formats = ['txt']" >> config.py echo 'allow_disabled_algorithms = True' >> config.py $sed -i.tmp 's,archive_older = [0-9],archive_older = 3,' config.py @@ -507,10 +498,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -cp $WORKSPACE/tests/keystore.jks $REPOROOT/ -$fdroid init --keystore keystore.jks --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore echo "accepted_formats = ['txt', 'yml']" >> config.py echo 'keydname = "CN=Birdman, OU=Cell, O=Alcatraz, L=Alcatraz, S=California, C=US"' >> config.py test -d metadata || mkdir metadata @@ -610,10 +598,10 @@ #------------------------------------------------------------------------------# -echo_header "create a source tarball and use that to build a repo" +echo_header "create a source tarball" cd $WORKSPACE -$python setup.py sdist +./setup.py compile_catalog sdist REPOROOT=`create_test_dir` cd $REPOROOT @@ -665,7 +653,7 @@ REPOROOT=`create_test_dir` cd $REPOROOT -$fdroid init +fdroid_init_with_prebuilt_keystore copy_apks_into_repo $REPOROOT $fdroid update --create-metadata --verbose $fdroid readmeta @@ -675,7 +663,7 @@ $fdroid server update --local-copy-dir=$LOCALCOPYDIR NEWREPOROOT=`create_test_dir` cd $NEWREPOROOT -$fdroid init +fdroid_init_with_prebuilt_keystore echo "sync_from_local_copy_dir = True" >> config.py $fdroid server update --local-copy-dir=$LOCALCOPYDIR @@ -787,11 +775,8 @@ echo_header "check duplicate files are properly handled by fdroid update" REPOROOT=`create_test_dir` -KEYSTORE=$WORKSPACE/tests/keystore.jks cd $REPOROOT -$fdroid init --keystore $KEYSTORE --repo-keyalias=sova -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py +fdroid_init_with_prebuilt_keystore mkdir $REPOROOT/metadata cp -a $WORKSPACE/tests/metadata/obb.mainpatch.current.txt $REPOROOT/metadata echo "accepted_formats = ['txt']" >> config.py @@ -813,7 +798,7 @@ cd $REPOROOT mkdir repo copy_apks_into_repo $REPOROOT -$fdroid init +fdroid_init_with_prebuilt_keystore $fdroid update --create-metadata --verbose $fdroid readmeta grep -F '> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py echo "binary_transparency_remote = '$GIT_REMOTE'" >> config.py echo "accepted_formats = ['json', 'txt', 'yml']" >> config.py $fdroid update --verbose @@ -976,7 +958,8 @@ REPOROOT=`create_test_dir` KEYSTORE=$REPOROOT/keystore.jks cd $REPOROOT -$fdroid init --keystore $KEYSTORE +cp $WORKSPACE/tests/keystore.jks $KEYSTORE +fdroid_init_with_prebuilt_keystore $KEYSTORE test -e $KEYSTORE cp $WORKSPACE/tests/urzip.apk $REPOROOT/repo/ $fdroid update --create-metadata --verbose @@ -1047,11 +1030,9 @@ fi cd $OFFLINE_ROOT -$fdroid init --keystore $KEYSTORE --repo-keyalias=sova +fdroid_init_with_prebuilt_keystore cp -a $WORKSPACE/tests/metadata $WORKSPACE/tests/repo $WORKSPACE/tests/stats $OFFLINE_ROOT/ -echo 'keystorepass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py -echo 'keypass = "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI="' >> config.py echo "mirrors = ['http://foo.bar/fdroid', 'http://asdflkdsfjafdsdfhkjh.onion/fdroid']" >> config.py echo "servergitmirrors = '$SERVER_GIT_MIRROR'" >> config.py echo "local_copy_dir = '$LOCAL_COPY_DIR'" >> config.py diff -Nru fdroidserver-0.9.1/tests/source-files/fdroid/fdroidclient/build.gradle fdroidserver-1.0.0/tests/source-files/fdroid/fdroidclient/build.gradle --- fdroidserver-0.9.1/tests/source-files/fdroid/fdroidclient/build.gradle 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/source-files/fdroid/fdroidclient/build.gradle 2017-12-28 21:49:56.000000000 +0000 @@ -129,6 +129,21 @@ compileSdkVersion 21 buildToolsVersion '22.0.1' + defaultConfig { + + flavorDimensions "default" + + productFlavors { + devVersion { + applicationId "org.fdroid.fdroid.dev" + dimension "default" + versionCode 949 + versionName "0.95-dev" + } + } + + } + sourceSets { main { manifest.srcFile 'AndroidManifest.xml' diff -Nru fdroidserver-0.9.1/tests/stats/known_apks.txt fdroidserver-1.0.0/tests/stats/known_apks.txt --- fdroidserver-0.9.1/tests/stats/known_apks.txt 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/stats/known_apks.txt 2017-12-28 22:07:26.000000000 +0000 @@ -1,7 +1,8 @@ -com.politedroid_3.apk repo/com.politedroid 2017-06-23 -com.politedroid_4.apk repo/com.politedroid 2017-06-23 -com.politedroid_5.apk repo/com.politedroid 2017-06-23 -com.politedroid_6.apk repo/com.politedroid 2017-06-23 +com.politedroid_3.apk com.politedroid 2017-06-23 +com.politedroid_4.apk com.politedroid 2017-06-23 +com.politedroid_5.apk com.politedroid 2017-06-23 +com.politedroid_6.apk com.politedroid 2017-06-23 +duplicate.permisssions_9999999.apk duplicate.permisssions 2017-12-22 fake.ota.update_1234.zip fake.ota.update 2016-03-10 obb.main.oldversion_1444412523.apk obb.main.oldversion 2013-12-31 obb.main.twoversions_1101613.apk obb.main.twoversions 2015-10-12 diff -Nru fdroidserver-0.9.1/tests/update.TestCase fdroidserver-1.0.0/tests/update.TestCase --- fdroidserver-0.9.1/tests/update.TestCase 2017-11-27 19:09:52.000000000 +0000 +++ fdroidserver-1.0.0/tests/update.TestCase 2017-12-28 22:07:26.000000000 +0000 @@ -32,6 +32,14 @@ class UpdateTest(unittest.TestCase): '''fdroid update''' + def setUp(self): + logging.basicConfig(level=logging.INFO) + self.basedir = os.path.join(localmodule, 'tests') + self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles')) + if not os.path.exists(self.tmpdir): + os.makedirs(self.tmpdir) + os.chdir(self.basedir) + def testInsertStoreMetadata(self): config = dict() fdroidserver.common.fill_config_defaults(config) @@ -42,19 +50,47 @@ shutil.rmtree(os.path.join('repo', 'info.guardianproject.urzip'), ignore_errors=True) + shutil.rmtree(os.path.join('build', 'com.nextcloud.client'), ignore_errors=True) + shutil.copytree(os.path.join('source-files', 'com.nextcloud.client'), + os.path.join('build', 'com.nextcloud.client')) + + shutil.rmtree(os.path.join('build', 'com.nextcloud.client.dev'), ignore_errors=True) + shutil.copytree(os.path.join('source-files', 'com.nextcloud.client.dev'), + os.path.join('build', 'com.nextcloud.client.dev')) + + shutil.rmtree(os.path.join('build', 'eu.siacs.conversations'), ignore_errors=True) + shutil.copytree(os.path.join('source-files', 'eu.siacs.conversations'), + os.path.join('build', 'eu.siacs.conversations')) + apps = dict() - for packageName in ('info.guardianproject.urzip', 'org.videolan.vlc', 'obb.mainpatch.current'): - apps[packageName] = dict() + for packageName in ('info.guardianproject.urzip', 'org.videolan.vlc', 'obb.mainpatch.current', + 'com.nextcloud.client', 'com.nextcloud.client.dev', + 'eu.siacs.conversations'): + apps[packageName] = fdroidserver.metadata.App() apps[packageName]['id'] = packageName apps[packageName]['CurrentVersionCode'] = 0xcafebeef + apps['info.guardianproject.urzip']['CurrentVersionCode'] = 100 + + buildnextcloudclient = fdroidserver.metadata.Build() + buildnextcloudclient.gradle = ['generic'] + apps['com.nextcloud.client']['builds'] = [buildnextcloudclient] + + buildnextclouddevclient = fdroidserver.metadata.Build() + buildnextclouddevclient.gradle = ['versionDev'] + apps['com.nextcloud.client.dev']['builds'] = [buildnextclouddevclient] + + build_conversations = fdroidserver.metadata.Build() + build_conversations.gradle = ['free'] + apps['eu.siacs.conversations']['builds'] = [build_conversations] + fdroidserver.update.insert_localized_app_metadata(apps) appdir = os.path.join('repo', 'info.guardianproject.urzip', 'en-US') self.assertTrue(os.path.isfile(os.path.join(appdir, 'icon.png'))) self.assertTrue(os.path.isfile(os.path.join(appdir, 'featureGraphic.png'))) - self.assertEqual(3, len(apps)) + self.assertEqual(6, len(apps)) for packageName, app in apps.items(): self.assertTrue('localized' in app) self.assertTrue('en-US' in app['localized']) @@ -77,17 +113,25 @@ self.assertEqual('featureGraphic.png', app['localized']['en-US']['featureGraphic']) self.assertEqual(1, len(app['localized']['en-US']['phoneScreenshots'])) self.assertEqual(1, len(app['localized']['en-US']['sevenInchScreenshots'])) + elif packageName == 'com.nextcloud.client': + self.assertEqual('Nextcloud', app['localized']['en-US']['name']) + self.assertEqual(1073, len(app['localized']['en-US']['description'])) + self.assertEqual(78, len(app['localized']['en-US']['summary'])) + elif packageName == 'com.nextcloud.client.dev': + self.assertEqual('Nextcloud Dev', app['localized']['en-US']['name']) + self.assertEqual(586, len(app['localized']['en-US']['description'])) + self.assertEqual(79, len(app['localized']['en-US']['summary'])) + elif packageName == 'eu.siacs.conversations': + self.assertEqual('Conversations', app['localized']['en-US']['name']) def test_insert_triple_t_metadata(self): - importer = os.path.join(localmodule, 'tests', 'tmp', 'importer') + importer = os.path.join(self.basedir, 'tmp', 'importer') packageName = 'org.fdroid.ci.test.app' if not os.path.isdir(importer): logging.warning('skipping test_insert_triple_t_metadata, import.TestCase must run first!') return - tmpdir = os.path.join(localmodule, '.testfiles') - if not os.path.exists(tmpdir): - os.makedirs(tmpdir) - tmptestsdir = tempfile.mkdtemp(prefix='test_insert_triple_t_metadata-', dir=tmpdir) + tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, + dir=self.tmpdir) packageDir = os.path.join(tmptestsdir, 'build', packageName) shutil.copytree(importer, packageDir) @@ -209,14 +253,14 @@ apps = fdroidserver.metadata.read_metadata(xref=True) knownapks = fdroidserver.common.KnownApks() apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) - self.assertEqual(len(apks), 11) + self.assertEqual(len(apks), 12) apk = apks[0] self.assertEqual(apk['packageName'], 'com.politedroid') self.assertEqual(apk['versionCode'], 3) self.assertEqual(apk['minSdkVersion'], '3') self.assertEqual(apk['targetSdkVersion'], '3') self.assertFalse('maxSdkVersion' in apk) - apk = apks[4] + apk = apks[5] self.assertEqual(apk['packageName'], 'obb.main.oldversion') self.assertEqual(apk['versionCode'], 1444412523) self.assertEqual(apk['minSdkVersion'], '4') @@ -349,10 +393,6 @@ self.assertEqual(apk, frompickle) def test_process_apk_signed_by_disabled_algorithms(self): - os.chdir(os.path.join(localmodule, 'tests')) - if os.path.basename(os.getcwd()) != 'tests': - raise Exception('This test must be run in the "tests/" subdir') - config = dict() fdroidserver.common.fill_config_defaults(config) fdroidserver.update.config = config @@ -370,12 +410,9 @@ fdroidserver.update.options.allow_disabled_algorithms = False knownapks = fdroidserver.common.KnownApks() - apksourcedir = os.getcwd() - tmpdir = os.path.join(localmodule, '.testfiles') - if not os.path.exists(tmpdir): - os.makedirs(tmpdir) - tmptestsdir = tempfile.mkdtemp(prefix='test_process_apk_signed_by_disabled_algorithms-', - dir=tmpdir) + + tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, + dir=self.tmpdir) print('tmptestsdir', tmptestsdir) os.chdir(tmptestsdir) os.mkdir('repo') @@ -386,7 +423,7 @@ disabledsigs = ['org.bitbucket.tickytacky.mirrormirror_2.apk', ] for apkName in disabledsigs: - shutil.copy(os.path.join(apksourcedir, apkName), + shutil.copy(os.path.join(self.basedir, apkName), os.path.join(tmptestsdir, 'repo')) skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo', @@ -437,7 +474,7 @@ badsigs = ['urzip-badcert.apk', 'urzip-badsig.apk', 'urzip-release-unsigned.apk', ] for apkName in badsigs: - shutil.copy(os.path.join(apksourcedir, apkName), + shutil.copy(os.path.join(self.basedir, apkName), os.path.join(tmptestsdir, 'repo')) skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo', @@ -490,7 +527,7 @@ knownapks = fdroidserver.common.KnownApks() apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False) fdroidserver.update.translate_per_build_anti_features(apps, apks) - self.assertEqual(len(apks), 11) + self.assertEqual(len(apks), 12) foundtest = False for apk in apks: if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3: @@ -501,11 +538,8 @@ self.assertTrue(foundtest) def test_create_metadata_from_template(self): - tmpdir = os.path.join(localmodule, '.testfiles') - if not os.path.exists(tmpdir): - os.makedirs(tmpdir) - tmptestsdir = tempfile.mkdtemp(prefix='test_create_metadata_from_template-', - dir=tmpdir) + tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, + dir=self.tmpdir) print('tmptestsdir', tmptestsdir) os.chdir(tmptestsdir) os.mkdir('repo') @@ -567,6 +601,35 @@ self.assertEqual('urzip', data['Name']) self.assertEqual('urzip', data['Summary']) + def test_has_known_vulnerability(self): + good = [ + 'org.bitbucket.tickytacky.mirrormirror_1.apk', + 'org.bitbucket.tickytacky.mirrormirror_2.apk', + 'org.bitbucket.tickytacky.mirrormirror_3.apk', + 'org.bitbucket.tickytacky.mirrormirror_4.apk', + 'org.dyndns.fules.ck_20.apk', + 'urzip.apk', + 'urzip-badcert.apk', + 'urzip-badsig.apk', + 'urzip-release.apk', + 'urzip-release-unsigned.apk', + 'repo/com.politedroid_3.apk', + 'repo/com.politedroid_4.apk', + 'repo/com.politedroid_5.apk', + 'repo/com.politedroid_6.apk', + 'repo/obb.main.oldversion_1444412523.apk', + 'repo/obb.mainpatch.current_1619_another-release-key.apk', + 'repo/obb.mainpatch.current_1619.apk', + 'repo/obb.main.twoversions_1101613.apk', + 'repo/obb.main.twoversions_1101615.apk', + 'repo/obb.main.twoversions_1101617.apk', + 'repo/urzip-; Рахма́нинов, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢尔盖·.apk', + ] + for f in good: + self.assertFalse(fdroidserver.update.has_known_vulnerability(f)) + with self.assertRaises(fdroidserver.exception.FDroidException): + fdroidserver.update.has_known_vulnerability('janus.apk') + if __name__ == "__main__": parser = optparse.OptionParser()