1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-10-05 18:50:09 +02:00

scanner: implement caching rules for suss

This commit is contained in:
Michael Pöhn 2022-09-30 04:44:14 +02:00
parent bfcc30b854
commit a8bcaa3d70
4 changed files with 78 additions and 57 deletions

View File

@ -987,7 +987,7 @@ def main():
if not options.appid and not options.all: if not options.appid and not options.all:
parser.error("option %s: If you really want to build all the apps, use --all" % "all") parser.error("option %s: If you really want to build all the apps, use --all" % "all")
config = common.read_config(options) config = common.read_config(opts=options)
if config['build_server_always']: if config['build_server_always']:
options.server = True options.server = True

View File

@ -324,19 +324,26 @@ def fill_config_defaults(thisconfig):
thisconfig['gradle_version_dir'] = str(Path(thisconfig['cachedir']) / 'gradle') thisconfig['gradle_version_dir'] = str(Path(thisconfig['cachedir']) / 'gradle')
def get_config(options=None): def get_config(opts=None):
""" """
helper function for getting access to commons.config while safely helper function for getting access to commons.config while safely
initializing if it wasn't initialized yet. initializing if it wasn't initialized yet.
""" """
global config global config, options
if config is not None: if config is not None:
return config return config
config = {} config = {}
common.fill_config_defaults(config) common.fill_config_defaults(config)
common.read_config(options) common.read_config(opts=opts)
# make sure these values are available in common.py even if they didn't
# declare global in a scope
common.config = config
if opts is not None:
common.options = opts
return config return config

View File

@ -141,6 +141,10 @@ class SignatureDataOutdatedException(Exception):
pass pass
class SignatureDataCacheMissException(Exception):
pass
class SignatureDataVersionMismatchException(Exception): class SignatureDataVersionMismatchException(Exception):
pass pass
@ -151,7 +155,7 @@ class SignatureDataController:
self.filename = filename self.filename = filename
self.url = url self.url = url
# by default we assume cache is valid indefinitely # by default we assume cache is valid indefinitely
self.cache_outdated_interval = timedelta(days=999999) self.cache_duration = timedelta(days=999999)
self.data = {} self.data = {}
def check_data_version(self): def check_data_version(self):
@ -162,63 +166,65 @@ class SignatureDataController:
''' '''
NOTE: currently not in use NOTE: currently not in use
Checks if the timestamp value is ok. Raises an exception if something Checks if the last_updated value is ok. Raises an exception if
is not ok. it's expired or inaccessible.
:raises SignatureDataMalformedException: when timestamp value is :raises SignatureDataMalformedException: when timestamp value is
inaccessible or not parse-able inaccessible or not parse-able
:raises SignatureDataOutdatedException: when timestamp is older then :raises SignatureDataOutdatedException: when timestamp is older then
`self.cache_outdated_interval` `self.cache_duration`
''' '''
timestamp = self.data.get("timestamp") last_updated = self.data.get("last_updated", None)
if not timestamp: if last_updated:
raise SignatureDataMalformedException() try:
try: last_updated = datetime.fromisoformat(last_updated)
timestamp = datetime.fromisoformat(timestamp) except ValueError as e:
except ValueError as e: raise SignatureDataMalformedException() from e
raise SignatureDataMalformedException() from e except TypeError as e:
except TypeError as e: raise SignatureDataMalformedException() from e
raise SignatureDataMalformedException() from e delta = (last_updated + self.cache_duration) - scanner._datetime_now()
if (timestamp + self.cache_outdated_interval) < scanner._datetime_now(): if delta > timedelta(seconds=0):
raise SignatureDataOutdatedException() logging.debug(_('next {name} cache update due in {time}').format(
name=self.filename, time=delta
))
else:
raise SignatureDataOutdatedException()
def fetch(self): def fetch(self):
try: try:
self.load_from_cache() self.fetch_signatures_from_web()
self.verify_data()
self.check_last_updated()
except (
SignatureDataMalformedException,
SignatureDataVersionMismatchException,
SignatureDataOutdatedException
):
try:
self.fetch_signatures_from_web()
except AttributeError:
# just load from defaults if fetch_signatures_from_web is not
# implemented
self.load_from_defaults()
self.write_to_cache() self.write_to_cache()
except Exception as e:
raise Exception(_("downloading scanner signatures from '{}' failed").format(self.url)) from e
def load(self): def load(self):
try: try:
self.load_from_cache() try:
self.verify_data() self.load_from_cache()
except (SignatureDataMalformedException, SignatureDataVersionMismatchException): self.verify_data()
self.load_from_defaults() self.check_last_updated()
except SignatureDataCacheMissException:
self.load_from_defaults()
except SignatureDataOutdatedException:
self.fetch_signatures_from_web()
self.write_to_cache() self.write_to_cache()
except (SignatureDataMalformedException, SignatureDataVersionMismatchException) as e:
logging.critical(_("scanner cache is malformed! You can clear it with: '{clear}'").format(
clear='rm -r {}'.format(common.get_config()['cachedir_scanner'])
))
raise e
def load_from_defaults(self): def load_from_defaults(self):
sig_file = (Path(__file__).parent / 'data' / 'scanner' / self.filename).resolve() sig_file = (Path(__file__).parent / 'data' / 'scanner' / self.filename).resolve()
with open(sig_file) as f: with open(sig_file) as f:
self.data = json.load(f) self.set_data(json.load(f))
def load_from_cache(self): def load_from_cache(self):
sig_file = scanner._scanner_cachedir() / self.filename sig_file = scanner._scanner_cachedir() / self.filename
if not sig_file.exists(): if not sig_file.exists():
raise SignatureDataMalformedException() raise SignatureDataCacheMissException()
with open(sig_file) as f: with open(sig_file) as f:
self.data = json.load(f) self.set_data(json.load(f))
def write_to_cache(self): def write_to_cache(self):
sig_file = scanner._scanner_cachedir() / self.filename sig_file = scanner._scanner_cachedir() / self.filename
@ -231,29 +237,36 @@ class SignatureDataController:
cleans and validates and cleans `self.data` cleans and validates and cleans `self.data`
''' '''
self.check_data_version() self.check_data_version()
valid_keys = ['timestamp', 'version', 'signatures'] valid_keys = ['timestamp', 'last_updated', 'version', 'signatures', 'cache_duration']
for k in list(self.data.keys()): for k in list(self.data.keys()):
if k not in valid_keys: if k not in valid_keys:
del self.data[k] del self.data[k]
def set_data(self, new_data):
self.data = new_data
if 'cache_duration' in new_data:
self.cache_duration = timedelta(seconds=new_data['cache_duration'])
def fetch_signatures_from_web(self): def fetch_signatures_from_web(self):
logging.debug(_("downloading '{}'").format(self.url)) logging.debug(_("downloading '{}'").format(self.url))
with urllib.request.urlopen(self.url) as f: with urllib.request.urlopen(self.url) as f:
self.data = json.load(f) self.set_data(json.load(f))
self.data['last_updated'] = scanner._datetime_now().isoformat()
class ExodusSignatureDataController(SignatureDataController): class ExodusSignatureDataController(SignatureDataController):
def __init__(self): def __init__(self):
super().__init__('Exodus signatures', 'exodus.yml', 'https://reports.exodus-privacy.eu.org/api/trackers') super().__init__('Exodus signatures', 'exodus.yml', 'https://reports.exodus-privacy.eu.org/api/trackers')
self.cache_outdated_interval = timedelta(days=1) # refresh exodus cache after one day self.cache_duration = timedelta(days=1) # refresh exodus cache after one day
def fetch_signatures_from_web(self): def fetch_signatures_from_web(self):
logging.debug(_("downloading '{}'").format(self.url)) logging.debug(_("downloading '{}'").format(self.url))
self.data = { data = {
"signatures": {}, "signatures": {},
"timestamp": scanner._datetime_now().isoformat(), "timestamp": scanner._datetime_now().isoformat(),
"last_updated": scanner._datetime_now().isoformat(),
"version": SCANNER_CACHE_VERSION, "version": SCANNER_CACHE_VERSION,
} }
@ -261,7 +274,7 @@ class ExodusSignatureDataController(SignatureDataController):
d = json.load(f) d = json.load(f)
for tracker in d["trackers"].values(): for tracker in d["trackers"].values():
if tracker.get('code_signature'): if tracker.get('code_signature'):
self.data["signatures"][tracker["name"]] = { data["signatures"][tracker["name"]] = {
"name": tracker["name"], "name": tracker["name"],
"warn_code_signatures": [tracker["code_signature"]], "warn_code_signatures": [tracker["code_signature"]],
# exodus also provides network signatures, unused atm. # exodus also provides network signatures, unused atm.
@ -273,6 +286,7 @@ class ExodusSignatureDataController(SignatureDataController):
# etc. might be listed by exodus # etc. might be listed by exodus
# too. # too.
} }
self.set_data(data)
class ScannerTool(): class ScannerTool():
@ -316,6 +330,7 @@ class ScannerTool():
def refresh(self): def refresh(self):
for sdc in self.sdcs: for sdc in self.sdcs:
sdc.fetch_signatures_from_web() sdc.fetch_signatures_from_web()
sdc.write_to_cache()
def add(self, new_controller: SignatureDataController): def add(self, new_controller: SignatureDataController):
self.sdcs.append(new_controller) self.sdcs.append(new_controller)
@ -684,7 +699,7 @@ def main():
logging.getLogger().setLevel(logging.ERROR) logging.getLogger().setLevel(logging.ERROR)
# initialize/load configuration values # initialize/load configuration values
common.get_config(options) common.get_config(opts=options)
if options.refresh: if options.refresh:
scanner._get_tool().refresh() scanner._get_tool().refresh()

View File

@ -502,39 +502,38 @@ class Test_SignatureDataController(unittest.TestCase):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json') sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
self.assertEqual(sdc.name, 'nnn') self.assertEqual(sdc.name, 'nnn')
self.assertEqual(sdc.filename, 'fff.yml') self.assertEqual(sdc.filename, 'fff.yml')
self.assertEqual(sdc.cache_outdated_interval, timedelta(999999)) self.assertEqual(sdc.cache_duration, timedelta(999999))
self.assertDictEqual(sdc.data, {}) self.assertDictEqual(sdc.data, {})
# check_last_updated # check_last_updated
def test_check_last_updated_ok(self): def test_check_last_updated_ok(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json') sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
sdc.data['timestamp'] = datetime.now().astimezone().isoformat() sdc.data['last_updated'] = datetime.now().astimezone().isoformat()
sdc.check_last_updated() sdc.check_last_updated()
def test_check_last_updated_exception_cache_outdated(self): def test_check_last_updated_exception_cache_outdated(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json') sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
sdc.cache_outdated_interval = timedelta(days=7) sdc.cache_duration = timedelta(days=7)
sdc.data['timestamp'] = (datetime.now().astimezone() - timedelta(days=30)).isoformat() sdc.data['last_updated'] = (datetime.now().astimezone() - timedelta(days=30)).isoformat()
with self.assertRaises(fdroidserver.scanner.SignatureDataOutdatedException): with self.assertRaises(fdroidserver.scanner.SignatureDataOutdatedException):
sdc.check_last_updated() sdc.check_last_updated()
def test_check_last_updated_exception_missing_timestamp_value(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException):
sdc.check_last_updated()
def test_check_last_updated_exception_not_string(self): def test_check_last_updated_exception_not_string(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json') sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
sdc.data['timestamp'] = 12345 sdc.data['last_updated'] = 12345
with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException): with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException):
sdc.check_last_updated() sdc.check_last_updated()
def test_check_last_updated_exception_not_iso_formatted_string(self): def test_check_last_updated_exception_not_iso_formatted_string(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json') sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
sdc.data['timestamp'] = '01/09/2002 10:11' sdc.data['last_updated'] = '01/09/2002 10:11'
with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException): with self.assertRaises(fdroidserver.scanner.SignatureDataMalformedException):
sdc.check_last_updated() sdc.check_last_updated()
def test_check_last_updated_no_exception_missing_when_last_updated_not_set(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')
sdc.check_last_updated()
# check_data_version # check_data_version
def test_check_data_version_ok(self): def test_check_data_version_ok(self):
sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json') sdc = fdroidserver.scanner.SignatureDataController('nnn', 'fff.yml', 'https://example.com/test.json')