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'
+
+VERCODE_OPERATION_RE = re.compile(r'^([ 0-9/*+-]|%c)+$')
# A signature block file with a .DSA, .RSA, or .EC extension
CERT_PATH_REGEX = re.compile(r'^META-INF/.*\.(DSA|EC|RSA)$')
default_config = {
'sdk_path': "$ANDROID_HOME",
'ndk_paths': {
- 'r9b': None,
'r10e': None,
'r11c': None,
'r12b': "$ANDROID_NDK",
'r13b': None,
'r14b': None,
'r15c': None,
- 'r16': None,
+ 'r16b': None,
},
- 'qt_sdk_path': None,
- 'build_tools': "25.0.2",
+ 'build_tools': MINIMUM_AAPT_VERSION,
'force_build_tools': False,
'java_paths': None,
'ant': "ant",
r'^java-([6-9])-oracle$', # Debian WebUpd8
r'^jdk-([6-9])-oracle-.*$', # Debian make-jpkg
r'^java-([6-9])-openjdk-[^c][^o][^m].*$', # Debian
+ r'^oracle-jdk-bin-1\.([7-9]).*$', # Gentoo (oracle)
+ r'^icedtea-bin-([7-9]).*$', # Gentoo (openjdk)
]:
m = re.match(regex, j)
if not m:
pathlist += glob.glob('/usr/java/jdk1.[6-9]*')
pathlist += glob.glob('/System/Library/Java/JavaVirtualMachines/1.[6-9].0.jdk')
pathlist += glob.glob('/Library/Java/JavaVirtualMachines/*jdk*[6-9]*')
+ pathlist += glob.glob('/opt/oracle-jdk-*1.[7-9]*')
+ pathlist += glob.glob('/opt/icedtea-*[7-9]*')
if os.getenv('JAVA_HOME') is not None:
pathlist.append(os.getenv('JAVA_HOME'))
if os.getenv('PROGRAMFILES') is not None:
# the Debian package has the version string like "v0.2-23.0.2"
too_old = False
if '.' in bugfix:
- if LooseVersion(bugfix) < LooseVersion('24.0.0'):
+ if LooseVersion(bugfix) < LooseVersion(MINIMUM_AAPT_VERSION):
too_old = True
- elif LooseVersion('.'.join((major, minor, bugfix))) < LooseVersion('0.2.2964546'):
+ 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-24.0.0 or newer!")
- .format(aapt=aapt))
+ 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)
#
# supported in git >= 2.3
git_config = [
- '-c', 'core.sshCommand=false',
+ '-c', 'core.askpass=/bin/true',
+ '-c', 'core.sshCommand=/bin/false',
'-c', 'url.https://.insteadOf=ssh://',
]
for domain in ('bitbucket.org', 'github.com', 'gitlab.com'):
git_config.append('url.https://u:p@' + domain + '.insteadOf=https://' + domain)
envs.update({
'GIT_TERMINAL_PROMPT': '0',
- 'GIT_SSH': 'false', # for git < 2.3
+ 'GIT_ASKPASS': '/bin/true',
+ 'SSH_ASKPASS': '/bin/true',
+ 'GIT_SSH': '/bin/false', # for git < 2.3
})
return FDroidPopen(['git', ] + git_config + args,
envs=envs, cwd=cwd, output=output)
def gotorevisionx(self, rev):
if not os.path.exists(self.local):
# Brand new checkout
- p = self.git(['clone', self.remote, self.local])
+ p = self.git(['clone', '--', self.remote, self.local])
if p.returncode != 0:
self.clone_failed = True
raise VCSException("Git clone failed", p.output)
if 'Multiple remote HEAD branches' not in lines[0]:
raise VCSException(_("Git remote set-head failed"), p.output)
branch = lines[1].split(' ')[-1]
- p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', branch], cwd=self.local, output=False)
+ p2 = FDroidPopen(['git', 'remote', 'set-head', 'origin', '--', branch],
+ cwd=self.local, output=False)
if p2.returncode != 0:
raise VCSException(_("Git remote set-head failed"), p.output + '\n' + p2.output)
self.refreshed = True
def git(self, args, envs=dict(), cwd=None, output=True):
'''Prevent git fetch/clone/submodule from hanging at the username/password prompt
+
+ AskPass is set to /bin/true to let the process try to connect
+ without a username/password.
+
+ The SSH command is set to /bin/false to block all SSH URLs
+ (supported in git >= 2.3). This protects against
+ CVE-2017-1000117.
+
'''
- # CVE-2017-1000117 block all SSH URLs (supported in git >= 2.3)
- config = ['-c', 'core.sshCommand=false']
+ git_config = [
+ '-c', 'core.askpass=/bin/true',
+ '-c', 'core.sshCommand=/bin/false',
+ ]
envs.update({
'GIT_TERMINAL_PROMPT': '0',
- 'GIT_SSH': 'false', # for git < 2.3
- 'SVN_SSH': 'false',
+ 'GIT_ASKPASS': '/bin/true',
+ 'SSH_ASKPASS': '/bin/true',
+ 'GIT_SSH': '/bin/false', # for git < 2.3
+ 'SVN_SSH': '/bin/false',
})
- return FDroidPopen(['git', ] + config + args,
+ return FDroidPopen(['git', ] + git_config + args,
envs=envs, cwd=cwd, output=output)
def gotorevisionx(self, rev):
if not os.path.exists(self.local):
# Brand new checkout
gitsvn_args = ['svn', 'clone']
+ remote = None
if ';' in self.remote:
remote_split = self.remote.split(';')
for i in remote_split[1:]:
gitsvn_args.extend(['-t', i[5:]])
elif i.startswith('branches='):
gitsvn_args.extend(['-b', i[9:]])
- gitsvn_args.extend([remote_split[0], self.local])
- p = self.git(gitsvn_args, output=False)
- if p.returncode != 0:
- self.clone_failed = True
- raise VCSException("Git svn clone failed", p.output)
+ remote = remote_split[0]
else:
- gitsvn_args.extend([self.remote, self.local])
- p = self.git(gitsvn_args, output=False)
- if p.returncode != 0:
- self.clone_failed = True
- raise VCSException("Git svn clone failed", p.output)
+ remote = self.remote
+
+ if not remote.startswith('https://'):
+ raise VCSException(_('HTTPS must be used with Subversion URLs!'))
+
+ # git-svn sucks at certificate validation, this throws useful errors:
+ import requests
+ r = requests.head(remote)
+ r.raise_for_status()
+ location = r.headers.get('location')
+ if location and not location.startswith('https://'):
+ raise VCSException(_('Invalid redirect to non-HTTPS: {before} -> {after} ')
+ .format(before=remote, after=location))
+
+ gitsvn_args.extend(['--', remote, self.local])
+ p = self.git(gitsvn_args)
+ if p.returncode != 0:
+ self.clone_failed = True
+ raise VCSException(_('git svn clone failed'), p.output)
self.checkrepo()
else:
self.checkrepo()
def gotorevisionx(self, rev):
if not os.path.exists(self.local):
- p = FDroidPopen(['hg', 'clone', '--ssh', 'false', self.remote, self.local], output=False)
+ p = FDroidPopen(['hg', 'clone', '--ssh', '/bin/false', '--', self.remote, self.local],
+ output=False)
if p.returncode != 0:
self.clone_failed = True
raise VCSException("Hg clone failed", p.output)
for line in p.output.splitlines():
if not line.startswith('? '):
raise VCSException("Unexpected output from hg status -uS: " + line)
- FDroidPopen(['rm', '-rf', line[2:]], cwd=self.local, output=False)
+ FDroidPopen(['rm', '-rf', '--', line[2:]], cwd=self.local, output=False)
if not self.refreshed:
- p = FDroidPopen(['hg', 'pull', '--ssh', 'false'], cwd=self.local, output=False)
+ p = FDroidPopen(['hg', 'pull', '--ssh', '/bin/false'], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Hg pull failed", p.output)
self.refreshed = True
rev = rev or 'default'
if not rev:
return
- p = FDroidPopen(['hg', 'update', '-C', rev], cwd=self.local, output=False)
+ p = FDroidPopen(['hg', 'update', '-C', '--', rev], cwd=self.local, output=False)
if p.returncode != 0:
raise VCSException("Hg checkout of '%s' failed" % rev, p.output)
p = FDroidPopen(['hg', 'purge', '--all'], cwd=self.local, output=False)
os.path.join(root, 'AndroidManifest.xml'))
-vcsearch_g = re.compile(r'''.*[Vv]ersionCode[ =]+["']*([0-9]+)["']*''').search
-vnsearch_g = re.compile(r'.*[Vv]ersionName *=* *(["\'])((?:(?=(\\?))\3.)*?)\1.*').search
-psearch_g = re.compile(r'.*(packageName|applicationId) *=* *["\']([^"]+)["\'].*').search
+vcsearch_g = re.compile(r'''.*[Vv]ersionCode\s*=?\s*["']*([0-9]+)["']*''').search
+vnsearch_g = re.compile(r'''.*[Vv]ersionName\s*=?\s*(["'])((?:(?=(\\?))\3.)*?)\1.*''').search
+psearch_g = re.compile(r'''.*(packageName|applicationId)\s*=*\s*["']([^"']+)["'].*''').search
def app_matches_packagename(app, package):
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)
if srclib["Prepare"]:
cmd = replace_config_vars(srclib["Prepare"], build)
- p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=libdir)
+ p = FDroidPopen(['bash', '-x', '-c', '--', cmd], cwd=libdir)
if p.returncode != 0:
raise BuildException("Error running prepare command for srclib %s"
% name, p.output)
cmd = replace_config_vars(build.init, build)
logging.info("Running 'init' commands in %s" % root_dir)
- p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
+ p = FDroidPopen(['bash', '-x', '-c', '--', cmd], cwd=root_dir)
if p.returncode != 0:
raise BuildException("Error running init command for %s:%s" %
(app.id, build.versionName), p.output)
libpath = os.path.relpath(libpath, root_dir)
cmd = cmd.replace('$$' + name + '$$', libpath)
- p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
+ p = FDroidPopen(['bash', '-x', '-c', '--', cmd], cwd=root_dir)
if p.returncode != 0:
raise BuildException("Error running prebuild command for %s:%s" %
(app.id, build.versionName), p.output)
return os.path.splitext(filename)[1].lower()[1:]
-def get_apk_debuggable_aapt(apkfile):
+def use_androguard():
+ """Report if androguard is available, and config its debug logging"""
+
+ try:
+ import androguard
+ if use_androguard.show_path:
+ logging.debug(_('Using androguard from "{path}"').format(path=androguard.__file__))
+ use_androguard.show_path = False
+ if options and options.verbose:
+ logging.getLogger("androguard.axml").setLevel(logging.INFO)
+ return True
+ except ImportError:
+ return False
+
+
+use_androguard.show_path = True
+
+
+def _get_androguard_APK(apkfile):
+ try:
+ from androguard.core.bytecodes.apk import APK
+ except ImportError:
+ raise FDroidException("androguard library is not installed and aapt not present")
+
+ return APK(apkfile)
+
+
+def ensure_final_value(packageName, arsc, value):
+ """Ensure incoming value is always the value, not the resid
+
+ androguard will sometimes return the Android "resId" aka
+ Resource ID instead of the actual value. This checks whether
+ the value is actually a resId, then performs the Android
+ Resource lookup as needed.
+
+ """
+ if value:
+ returnValue = value
+ if value[0] == '@':
+ try: # can be a literal value or a resId
+ res_id = int('0x' + value[1:], 16)
+ res_id = arsc.get_id(packageName, res_id)[1]
+ returnValue = arsc.get_string(packageName, res_id)[1]
+ except ValueError:
+ pass
+ return returnValue
+
+
+def is_apk_and_debuggable_aapt(apkfile):
p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
output=False)
if p.returncode != 0:
return False
-def get_apk_debuggable_androguard(apkfile):
- try:
- from androguard.core.bytecodes.apk import APK
- except ImportError:
- raise FDroidException("androguard library is not installed and aapt not present")
-
- apkobject = APK(apkfile)
+def is_apk_and_debuggable_androguard(apkfile):
+ apkobject = _get_androguard_APK(apkfile)
if apkobject.is_valid_APK():
debuggable = apkobject.get_element("application", "debuggable")
if debuggable is not None:
return False
-def isApkAndDebuggable(apkfile):
+def is_apk_and_debuggable(apkfile):
"""Returns True if the given file is an APK and is debuggable
:param apkfile: full path to the apk to check"""
if get_file_extension(apkfile) != 'apk':
return False
- if SdkToolsPopen(['aapt', 'version'], output=False):
- return get_apk_debuggable_aapt(apkfile)
+ if use_androguard():
+ return is_apk_and_debuggable_androguard(apkfile)
else:
- return get_apk_debuggable_androguard(apkfile)
+ return is_apk_and_debuggable_aapt(apkfile)
-def get_apk_id_aapt(apkfile):
- """Extrat identification information from APK using aapt.
+def get_apk_id(apkfile):
+ """Extract identification information from APK using aapt.
:param apkfile: path to an APK file.
:returns: triplet (appid, version code, version name)
"""
- r = re.compile("package: name='(?P<appid>.*)' versionCode='(?P<vercode>.*)' versionName='(?P<vername>.*)' platformBuildVersionName='.*'")
+ if use_androguard():
+ return get_apk_id_androguard(apkfile)
+ else:
+ return get_apk_id_aapt(apkfile)
+
+
+def get_apk_id_androguard(apkfile):
+ if not os.path.exists(apkfile):
+ raise FDroidException(_("Reading packageName/versionCode/versionName failed, APK invalid: '{apkfilename}'")
+ .format(apkfilename=apkfile))
+ a = _get_androguard_APK(apkfile)
+ versionName = ensure_final_value(a.package, a.get_android_resources(), a.get_androidversion_name())
+ if not versionName:
+ versionName = '' # versionName is expected to always be a str
+ return a.package, a.get_androidversion_code(), versionName
+
+
+def get_apk_id_aapt(apkfile):
+ r = re.compile("^package: name='(?P<appid>.*)' versionCode='(?P<vercode>.*)' versionName='(?P<vername>.*?)'(?: platformBuildVersionName='.*')?")
p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
for line in p.output.splitlines():
m = r.match(line)
p = None
try:
p = subprocess.Popen(commands, cwd=cwd, shell=False, env=process_env,
- stdout=subprocess.PIPE, stderr=stderr_param)
+ stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=stderr_param)
except OSError as e:
raise BuildException("OSError while trying to execute " +
' '.join(commands) + ': ' + str(e))
cmd = cmd.replace('$$SDK$$', config['sdk_path'])
cmd = cmd.replace('$$NDK$$', build.ndk_path())
cmd = cmd.replace('$$MVN3$$', config['mvn3'])
- cmd = cmd.replace('$$QT$$', config['qt_sdk_path'] or '')
if build is not None:
cmd = replace_build_vars(cmd, build)
return cmd
o.write('android.library.reference.%d=%s\n' % (number, relpath))
-apk_sigfile = re.compile(r'META-INF/[0-9A-Za-z]+\.(SF|RSA|DSA|EC)')
+apk_sigfile = re.compile(r'META-INF/[0-9A-Za-z_\-]+\.(SF|RSA|DSA|EC)')
def signer_fingerprint_short(sig):
if get_minSdkVersion_aapt(unsigned_path) < 18:
signature_algorithm = ['-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1']
else:
- signature_algorithm = ['-sigalg', 'SHA256withRSA', '-digestalg', 'SHA256']
+ signature_algorithm = ['-sigalg', 'SHA256withRSA', '-digestalg', 'SHA-256']
p = FDroidPopen([config['jarsigner'], '-keystore', config['keystore'],
'-storepass:env', 'FDROID_KEY_STORE_PASS',
jarsigner passes unsigned APKs as "verified"! So this has to turn
on -strict then check for result 4.
+ Just to be safe, this never reuses the file, and locks down the
+ file permissions while in use. That should prevent a bad actor
+ from changing the settings during operation.
+
:returns: boolean whether the APK was verified
+
"""
_java_security = os.path.join(os.getcwd(), '.java.security')
+ if os.path.exists(_java_security):
+ os.remove(_java_security)
with open(_java_security, 'w') as fp:
fp.write('jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024')
+ os.chmod(_java_security, 0o400)
try:
cmd = [
else:
logging.debug(_('JAR signature verified: {path}').format(path=apk))
return True
+ finally:
+ if os.path.exists(_java_security):
+ os.chmod(_java_security, 0o600)
+ os.remove(_java_security)
logging.error(_('Old APK signature failed to verify: {path}').format(path=apk)
+ '\n' + output.decode('utf-8'))
examplesdir = prefix + '/examples'
return examplesdir
+
+
+def get_wiki_timestamp(timestamp=None):
+ """Return current time in the standard format for posting to the wiki"""
+
+ if timestamp is None:
+ timestamp = time.gmtime()
+ return time.strftime("%Y-%m-%d %H:%M:%SZ", timestamp)
+
+
+def get_android_tools_versions(ndk_path=None):
+ '''get a list of the versions of all installed Android SDK/NDK components'''
+
+ global config
+ sdk_path = config['sdk_path']
+ if sdk_path[-1] != '/':
+ sdk_path += '/'
+ components = []
+ if ndk_path:
+ ndk_release_txt = os.path.join(ndk_path, 'RELEASE.TXT')
+ if os.path.isfile(ndk_release_txt):
+ with open(ndk_release_txt, 'r') as fp:
+ components.append((os.path.basename(ndk_path), fp.read()[:-1]))
+
+ pattern = re.compile('^Pkg.Revision=(.+)', re.MULTILINE)
+ for root, dirs, files in os.walk(sdk_path):
+ if 'source.properties' in files:
+ source_properties = os.path.join(root, 'source.properties')
+ with open(source_properties, 'r') as fp:
+ m = pattern.search(fp.read())
+ if m:
+ components.append((root[len(sdk_path):], m.group(1)))
+
+ return components
+
+
+def get_android_tools_version_log(ndk_path=None):
+ '''get a list of the versions of all installed Android SDK/NDK components'''
+ log = '== Installed Android Tools ==\n\n'
+ components = get_android_tools_versions(ndk_path)
+ for name, version in sorted(components):
+ log += '* ' + name + ' (' + version + ')\n'
+
+ return log
+
+
+def get_git_describe_link():
+ """Get a link to the current fdroiddata commit, to post to the wiki
+
+ """
+ try:
+ output = subprocess.check_output(['git', 'describe', '--always', '--dirty', '--abbrev=0'],
+ universal_newlines=True).strip()
+ except subprocess.CalledProcessError:
+ pass
+ if output:
+ commit = output.replace('-dirty', '')
+ return ('* fdroiddata: [https://gitlab.com/fdroid/fdroiddata/commit/{commit} {id}]\n'
+ .format(commit=commit, id=output))
+ else:
+ logging.error(_("'{path}' failed to execute!").format(path='git describe'))
+ return ''