diff --git a/core/Errors.py b/core/Errors.py index 6849f69..f492a8a 100644 --- a/core/Errors.py +++ b/core/Errors.py @@ -14,6 +14,10 @@ class ProfileDeactivationError(Exception): pass +class UnsupportedApplicationVersionError(Exception): + pass + + class MissingSubscriptionError(Exception): pass @@ -36,3 +40,7 @@ class InvoicePaymentFailedError(Exception): class ConnectionUnprotectedError(Exception): pass + + +class FileIntegrityError(Exception): + pass diff --git a/core/controllers/ApplicationVersionController.py b/core/controllers/ApplicationVersionController.py index ebe0430..bc9a451 100644 --- a/core/controllers/ApplicationVersionController.py +++ b/core/controllers/ApplicationVersionController.py @@ -1,3 +1,4 @@ +from core.Errors import FileIntegrityError, UnsupportedApplicationVersionError from core.controllers.ApplicationController import ApplicationController from core.models.session.ApplicationVersion import ApplicationVersion from core.observers.ApplicationVersionObserver import ApplicationVersionObserver @@ -5,6 +6,7 @@ from core.observers.ConnectionObserver import ConnectionObserver from core.services.WebServiceApiService import WebServiceApiService from io import BytesIO from typing import Optional +import hashlib import requests import shutil import tarfile @@ -35,6 +37,9 @@ class ApplicationVersionController: @staticmethod def install(application_version: ApplicationVersion, application_version_observer: Optional[ApplicationVersionObserver] = None, connection_observer: Optional[ConnectionObserver] = None): + if not application_version.is_supported(): + raise UnsupportedApplicationVersionError('The application version in question is not supported.') + from core.controllers.ConnectionController import ConnectionController ConnectionController.with_preferred_connection(application_version, task=ApplicationVersionController._install, application_version_observer=application_version_observer, connection_observer=connection_observer) @@ -54,8 +59,27 @@ class ApplicationVersionController: response = requests.get(application_version.download_path, stream=True) if response.status_code == 200: - with tarfile.open(fileobj=BytesIO(response.content), mode='r:gz') as file: - file.extractall(application_version.installation_path) + + file_hash = ApplicationVersionController._calculate_file_hash(BytesIO(response.content)) + + if file_hash != application_version.file_hash: + raise FileIntegrityError('Application version file integrity could not be verified.') + + file = tarfile.open(fileobj=BytesIO(response.content), mode = 'r:gz') + file.extractall(application_version.installation_path) else: raise ConnectionError('The application version could not be downloaded.') + + @staticmethod + def _calculate_file_hash(file): + + hasher = hashlib.sha3_512() + buffer = file.read(65536) + + while len(buffer) > 0: + + hasher.update(buffer) + buffer = file.read(65536) + + return hasher.hexdigest() diff --git a/core/controllers/ProfileController.py b/core/controllers/ProfileController.py index c64b6f2..a818e30 100644 --- a/core/controllers/ProfileController.py +++ b/core/controllers/ProfileController.py @@ -51,7 +51,7 @@ class ProfileController: application_version = profile.application_version - if not application_version.installed: + if not application_version.is_installed(): ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer) port_number = ConnectionController.establish_connection(profile, force=force, connection_observer=connection_observer) diff --git a/core/models/session/ApplicationVersion.py b/core/models/session/ApplicationVersion.py index bc1be65..a5df85b 100644 --- a/core/models/session/ApplicationVersion.py +++ b/core/models/session/ApplicationVersion.py @@ -14,8 +14,10 @@ _table_definition: str = """ 'id' int UNIQUE, 'application_code' varchar, 'version_number' varchar, + 'format_revision' int, 'download_path' varchar UNIQUE, 'released_at' varchar, + 'file_hash' varchar, UNIQUE(application_code, version_number) """ @@ -24,6 +26,10 @@ _table_definition: str = """ class ApplicationVersion(Model): application_code: str version_number: str + format_revision: Optional[int] = field( + default=None, + metadata=config(exclude=Exclude.ALWAYS) + ) id: Optional[int] = field( default=None, metadata=config(exclude=Exclude.ALWAYS) @@ -41,23 +47,23 @@ class ApplicationVersion(Model): exclude=Exclude.ALWAYS ) ) - installation_path: Optional[str] = field( + file_hash: Optional[str] = field( default=None, metadata=config(exclude=Exclude.ALWAYS) ) - installed: Optional[bool] = field( - default=False, - metadata=config(exclude=Exclude.ALWAYS) - ) - supported: Optional[bool] = field( - default=False, + installation_path: Optional[str] = field( + default=None, metadata=config(exclude=Exclude.ALWAYS) ) def __post_init__(self): self.installation_path = Constants.SP_APPLICATION_DATA_HOME + '/' + self.application_code + '/' + self.version_number - self.installed = os.path.isdir(self.installation_path) and len(os.listdir(self.installation_path)) > 0 - self.supported = self.exists(self.application_code, self.version_number) + + def is_installed(self): + return os.path.isdir(self.installation_path) and len(os.listdir(self.installation_path)) > 0 + + def is_supported(self): + return self.exists(self.application_code, self.version_number) and self.format_revision == 1 @staticmethod def find_by_id(id: int): @@ -81,7 +87,7 @@ class ApplicationVersion(Model): @staticmethod def save_many(application_versions): Model._create_table_if_not_exists(table_name=_table_name, table_definition=_table_definition) - Model._insert_many('INSERT INTO application_versions VALUES(?, ?, ?, ?, ?)', ApplicationVersion.tuple_factory, application_versions) + Model._insert_many('INSERT INTO application_versions VALUES(?, ?, ?, ?, ?, ?, ?)', ApplicationVersion.tuple_factory, application_versions) @staticmethod def factory(cursor, row): @@ -95,4 +101,4 @@ class ApplicationVersion(Model): @staticmethod def tuple_factory(application_version): - return application_version.id, application_version.application_code, application_version.version_number, application_version.download_path, application_version.released_at + return application_version.id, application_version.application_code, application_version.version_number, application_version.format_revision, application_version.download_path, application_version.released_at, application_version.file_hash diff --git a/core/services/WebServiceApiService.py b/core/services/WebServiceApiService.py index 3ac6186..0e68ab7 100644 --- a/core/services/WebServiceApiService.py +++ b/core/services/WebServiceApiService.py @@ -35,7 +35,7 @@ class WebServiceApiService: if response.status_code == requests.codes.ok: for application_version in response.json()['data']: - application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['id'], application_version['download_path'], application_version['released_at'])) + application_versions.append(ApplicationVersion(code, application_version['version_number'], application_version['format_revision'], application_version['id'], application_version['download_path'], application_version['released_at'], application_version['file_hash'])) return application_versions