import tempfile
import urllib.parse
import zipfile
+import calendar
from binascii import hexlify, unhexlify
from datetime import datetime
from xml.dom.minidom import Document
from . import metadata
from . import net
from . import signindex
-from fdroidserver.common import FDroidPopen, FDroidPopenBytes
+from fdroidserver.common import FDroidPopen, FDroidPopenBytes, load_stats_fdroid_signing_key_fingerprints
from fdroidserver.exception import FDroidException, VerificationException, MetaDataException
return "fdroid.app:" + appid, apps[appid].Name
raise MetaDataException("Cannot resolve app id " + appid)
- nosigningkey = False
if not common.options.nosign:
- if 'repo_keyalias' not in common.config:
- nosigningkey = True
- logging.critical(_("'repo_keyalias' not found in config.py!"))
- if 'keystore' not in common.config:
- nosigningkey = True
- logging.critical(_("'keystore' not found in config.py!"))
- if 'keystorepass' not in common.config:
- nosigningkey = True
- logging.critical(_("'keystorepass' not found in config.py!"))
- if 'keypass' not in common.config:
- nosigningkey = True
- logging.critical(_("'keypass' not found in config.py!"))
- if not os.path.exists(common.config['keystore']):
- nosigningkey = True
- logging.critical("'" + common.config['keystore'] + "' does not exist!")
- if nosigningkey:
- raise FDroidException("`fdroid update` requires a signing key, " +
- "you can create one using: fdroid update --create-key")
+ common.assert_config_keystore(common.config)
repodict = collections.OrderedDict()
repodict['timestamp'] = datetime.utcnow()
raise TypeError(_('only accepts strings, lists, and tuples'))
requestsdict[command] = packageNames
- make_v0(appsWithPackages, apks, repodir, repodict, requestsdict)
- make_v1(appsWithPackages, apks, repodir, repodict, requestsdict)
+ fdroid_signing_key_fingerprints = load_stats_fdroid_signing_key_fingerprints()
+ make_v0(appsWithPackages, apks, repodir, repodict, requestsdict,
+ fdroid_signing_key_fingerprints)
+ make_v1(appsWithPackages, apks, repodir, repodict, requestsdict,
+ fdroid_signing_key_fingerprints)
-def make_v1(apps, packages, repodir, repodict, requestsdict):
+
+def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
def _index_encoder_default(obj):
if isinstance(obj, set):
- return list(obj)
+ return sorted(list(obj))
if isinstance(obj, datetime):
- return int(obj.timestamp() * 1000) # Java expects milliseconds
+ # Java prefers milliseconds
+ # we also need to accound for time zone/daylight saving time
+ return int(calendar.timegm(obj.timetuple()) * 1000)
+ if isinstance(obj, dict):
+ d = collections.OrderedDict()
+ for key in sorted(obj.keys()):
+ d[key] = obj[key]
+ return d
raise TypeError(repr(obj) + " is not JSON serializable")
output = collections.OrderedDict()
output['repo'] = repodict
output['requests'] = requestsdict
+ # establish sort order of the index
+ v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints)
+
appslist = []
output['apps'] = appslist
for packageName, appdict in apps.items():
k = k[:1].lower() + k[1:]
d[k] = v
+ # establish sort order in localized dicts
+ for app in output['apps']:
+ localized = app.get('localized')
+ if localized:
+ lordered = collections.OrderedDict()
+ for lkey, lvalue in sorted(localized.items()):
+ lordered[lkey] = collections.OrderedDict()
+ for ikey, iname in sorted(lvalue.items()):
+ lordered[lkey][ikey] = iname
+ app['localized'] = lordered
+
output_packages = collections.OrderedDict()
output['packages'] = output_packages
for package in packages:
if packageName not in apps:
logging.info(_('Ignoring package without metadata: ') + package['apkName'])
continue
+ if not package.get('versionName'):
+ app = apps[packageName]
+ versionCodeStr = str(package['versionCode']) # TODO build.versionCode should be int!
+ for build in app['builds']:
+ if build['versionCode'] == versionCodeStr:
+ versionName = build.get('versionName')
+ logging.info(_('Overriding blank versionName in {apkfilename} from metadata: {version}')
+ .format(apkfilename=package['apkName'], version=versionName))
+ package['versionName'] = versionName
+ break
if packageName in output_packages:
packagelist = output_packages[packageName]
else:
signindex.sign_index_v1(repodir, json_name)
-def make_v0(apps, apks, repodir, repodict, requestsdict):
+def v1_sort_packages(packages, repodir, fdroid_signing_key_fingerprints):
+ """Sorts the supplied list to ensure a deterministic sort order for
+ package entries in the index file. This sort-order also expresses
+ installation preference to the clients.
+ (First in this list = first to install)
+
+ :param packages: list of packages which need to be sorted before but into index file.
+ """
+
+ GROUP_DEV_SIGNED = 1
+ GROUP_FDROID_SIGNED = 2
+ GROUP_OTHER_SIGNED = 3
+
+ def v1_sort_keys(package):
+ packageName = package.get('packageName', None)
+
+ sig = package.get('signer', None)
+
+ dev_sig = common.metadata_find_developer_signature(packageName)
+ group = GROUP_OTHER_SIGNED
+ if dev_sig and dev_sig == sig:
+ group = GROUP_DEV_SIGNED
+ else:
+ fdroidsig = fdroid_signing_key_fingerprints.get(packageName, {}).get('signer')
+ if fdroidsig and fdroidsig == sig:
+ group = GROUP_FDROID_SIGNED
+
+ versionCode = None
+ if package.get('versionCode', None):
+ versionCode = -int(package['versionCode'])
+
+ return(packageName, group, sig, versionCode)
+
+ packages.sort(key=v1_sort_keys)
+
+
+def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fingerprints):
"""
aka index.jar aka index.xml
"""
# Get a list of the apks for this app...
apklist = []
- versionCodes = []
+ apksbyversion = collections.defaultdict(lambda: [])
for apk in apks:
- if apk['packageName'] == appid:
- if apk['versionCode'] not in versionCodes:
- apklist.append(apk)
- versionCodes.append(apk['versionCode'])
+ if apk.get('versionCode') and apk.get('packageName') == appid:
+ apksbyversion[apk['versionCode']].append(apk)
+ for versionCode, apksforver in apksbyversion.items():
+ fdroidsig = fdroid_signing_key_fingerprints.get(appid, {}).get('signer')
+ fdroid_signed_apk = None
+ name_match_apk = None
+ for x in apksforver:
+ if fdroidsig and x.get('signer', None) == fdroidsig:
+ fdroid_signed_apk = x
+ if common.apk_release_filename.match(x.get('apkName', '')):
+ name_match_apk = x
+ # choose which of the available versions is most
+ # suiteable for index v0
+ if fdroid_signed_apk:
+ apklist.append(fdroid_signed_apk)
+ elif name_match_apk:
+ apklist.append(name_match_apk)
+ else:
+ apklist.append(apksforver[0])
if len(apklist) == 0:
continue
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
apkel = doc.createElement("package")
apel.appendChild(apkel)
- addElement('version', apk['versionName'], doc, apkel)
+
+ versionName = apk.get('versionName')
+ if not versionName:
+ versionCodeStr = str(apk['versionCode']) # TODO build.versionCode should be int!
+ for build in app.builds:
+ if build['versionCode'] == versionCodeStr and 'versionName' in build:
+ versionName = build['versionName']
+ break
+ if versionName:
+ addElement('version', versionName, doc, apkel)
+
addElement('versioncode', str(apk['versionCode']), doc, apkel)
addElement('apkname', apk['apkName'], doc, apkel)
addElementIfInApk('srcname', apk, 'srcname', doc, apkel)
segments.extend([branch, folder])
urls.append('/'.join(segments))
elif hostname == "gitlab.com":
- # Gitlab Raw "https://gitlab.com/user/repo/raw/branch/folder"
- gitlab_raw = segments + ['raw', branch, folder]
- urls.append('/'.join(gitlab_raw))
+ # Both these Gitlab URLs will work with F-Droid, but only the first will work in the browser
+ # This is because the `raw` URLs are not served with the correct mime types, so any
+ # index.html which is put in the repo will not be rendered. Putting an index.html file in
+ # the repo root is a common way for to make information about the repo available to end user.
+
# Gitlab-like Pages segments "https://user.gitlab.io/repo/folder"
gitlab_pages = ["https:", "", user + ".gitlab.io", repo, folder]
urls.append('/'.join(gitlab_pages))
+ # Gitlab Raw "https://gitlab.com/user/repo/raw/branch/folder"
+ gitlab_raw = segments + ['raw', branch, folder]
+ urls.append('/'.join(gitlab_raw))
return urls
return urls
jar = zipfile.ZipFile(fp)
# verify that the JAR signature is valid
- verify_jar_signature(fp.name)
+ logging.debug(_('Verifying index signature:'))
+ common.verify_jar_signature(fp.name)
# get public key and its fingerprint from JAR
public_key, public_key_fingerprint = get_public_key_from_jar(jar)
return index, new_etag
-def verify_jar_signature(file):
- """
- Verifies the signature of a given JAR file.
-
- :raises: VerificationException() if the JAR's signature could not be verified
- """
- if not common.verify_apk_signature(file, jar=True):
- raise VerificationException(_("The repository's index could not be verified."))
-
-
def get_public_key_from_jar(jar):
"""
Get the public key and its fingerprint from a JAR file.