def query_server(): # Builds the request update message request_update_dict = dict(type=messages.MessageType.REQUEST_UPDATE, crc32=0) request_update_message = messages.REQUEST_UPDATE_MESSAGE.build( request_update_dict) # Updates the crc request_update_dict["crc32"] = messages.calculate_crc( request_update_message) request_update_message = messages.REQUEST_UPDATE_MESSAGE.build( request_update_dict) ip_address = registry.get_value(settings.UPDATING_SERVER_REGISTRY) port = registry.get_value(settings.PORT_REGISTRY) sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sender.settimeout(settings.CONNECTION_TIMEOUT) try: # Sends the message sender.sendto(request_update_message, (ip_address, port)) print(f"Sent a version query to the server: {ip_address}:{port}") except socket.error: print("Error: Failed to check for update") finally: sender.shutdown(2) sender.close()
def handle_request_update(self): current_version = Version.get_current_version() update_path = registry.get_value( current_version.get_update_registry_path()) if not os.path.exists(update_path): logging.error( "Update file does not exist! Updates will not be available.") return # Sign the update file hash_object = settings.HASH_MODULE() update_size = os.stat(update_path).st_size bytes_read = 0 with open(update_path, "rb") as update: while bytes_read < update_size: data = update.read(settings.VERSION_CHUNK_SIZE) hash_object.update(data) bytes_read += len(data) version_update_dict = dict( type=MessageType.VERSION_UPDATE, header_signature=0, major=current_version.major, minor=current_version.minor, size=update_size, update_signature=rsa_signing.sign_hash(hash_object), spread=False) try: version_update_message = messages.VERSION_UPDATE_MESSAGE.build( version_update_dict) # Update signature version_update_dict["header_signature"] = messages.sign_message( version_update_message) version_update_message = messages.VERSION_UPDATE_MESSAGE.build( version_update_dict) except construct.ConstructError: # Should never occur logging.critical(f"Failed to build version update message", exc_info=True) return sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sender.settimeout(settings.CONNECTION_TIMEOUT) port = registry.get_value(settings.PORT_REGISTRY) try: # Sends the message sender.sendto(version_update_message, (self.sender[0], port)) logging.info( f"Sent version {current_version} to {self.sender[0]}.") except socket.error: logging.error("Unknown error while sending update message :(", exc_info=True) finally: sender.shutdown(2) sender.close()
def validate_hash(hash_object, signature, public_key=None, n=None): if public_key is None: public_key = int(registry.get_value(settings.RSA_PUBLIC_REGISTRY), 16) if n is None: n = int(registry.get_value(settings.RSA_MODULO_REGISTRY), 16) calculated_hash = int.from_bytes(hash_object.digest(), byteorder='big') hash_from_signature = pow(signature, public_key, n) return calculated_hash == hash_from_signature
def sign_hash(hash_object, private_key=None, n=None): if private_key is None: private_key = int(registry.get_value(settings.RSA_PRIVATE_REGISTRY), 16) if n is None: n = int(registry.get_value(settings.RSA_MODULO_REGISTRY), 16) calculated_hash = int.from_bytes(hash_object.digest(), byteorder='big') signature = pow(calculated_hash, private_key, n) return signature
def get_layout(): automatic_installation = registry.get_value( settings.AUTO_INSTALLATIONS_REGISTRY) update_frame = [ [sg.Button("Query server for update", key="-QUERY_SERVER-")], [ sg.CB("Automatically update on next launch", key="-AUTO-", enable_events=True, default=automatic_installation) ], ] installed_version = updater.Version.get_installed_version() update_version = updater.Version.get_current_version() layout = [[ sg.Button("Launch", key="-LAUNCH-", size=(15, 2)), sg.Button("Update", key="-UPDATE-", size=(15, 2), disabled=(update_version <= installed_version)) ], [ sg.T("Version: " + str(installed_version), key="-INSTALL_VERSION-", size=(15, 1)), sg.T("Update: " + str(update_version), key="-UPDATE_VERSION-", size=(15, 1)) ], [sg.Frame("Updater", update_frame)]] return layout
def install_update(silent): # Checks if an update is available if updater.Version.is_updated(): return "ERROR: No update is available." # Get the path of the update file version_registry = updater.Version.get_current_version( ).get_update_registry_path() update_filepath = registry.get_value(version_registry) # Replace the old update file with the new one error_message = None try: if not progressive_copy(update_filepath, settings.UPDATE_PATH, silent): # Update was canceled by the user return except PermissionError: error_message = f"Failed to replace update file {settings.UPDATE_PATH}. Insufficient permission or file is locked." except OSError: error_message = f"Failed to replace update file {settings.UPDATE_PATH}. Unknown OS Error." if error_message: if silent: print(error_message) else: sg.popup_error(error_message, title="Error") return # Extracts the update if update(settings.UPDATE_PATH, silent): # Version was updated! Update the registry updater.Version.get_current_version().update_installed_version() return "Update installed successfully!"
def show_server_information(): address_id = "Not available" if registry.exists(settings.ADDRESS_ID_REGISTRY): address_id = registry.get_value(settings.ADDRESS_ID_REGISTRY) ip = "Not available" if registry.exists(settings.UPDATING_SERVER_REGISTRY): ip = registry.get_value(settings.UPDATING_SERVER_REGISTRY) port = "Not available" if registry.exists(settings.PORT_REGISTRY): port = registry.get_value(settings.PORT_REGISTRY) print(f"Address ID: \t{address_id}") print(f"Domain (IP): \t{ip}") print(f"Port: \t\t{port}") return True
def broadcast_update_version(spread=True): # Gets the update info and update file update_version = updater.Version.get_current_version() version_registry = update_version.get_update_registry_path() update_filepath = registry.get_value(version_registry) # Tries to open update file (might fail if clients are downloading it) try: file_size = os.stat(update_filepath).st_size update_file = open(update_filepath, "rb") except PermissionError: print( f"Failed to write update file {settings.UPDATE_PATH}. Insufficient permission or file is locked." ) return False # Calculates size and signature of update data_received = 0 hash_object = settings.HASH_MODULE() # Calculate update signature with update_file: while data_received < file_size: chunk = update_file.read(settings.VERSION_CHUNK_SIZE) hash_object.update(chunk) data_received += len(chunk) signature = rsa_signing.sign_hash(hash_object) # Creates broadcast message version_update_dict = dict(type=MessageType.VERSION_UPDATE, header_signature=0, major=update_version.major, minor=update_version.minor, size=file_size, update_signature=signature, spread=spread) # Calculate signature of message try: version_update_message = messages.VERSION_UPDATE_MESSAGE.build( version_update_dict) # Update signature version_update_dict["header_signature"] = messages.sign_message( version_update_message) print(f"Spread: {version_update_dict['spread']}") version_update_message = messages.VERSION_UPDATE_MESSAGE.build( version_update_dict) except construct.ConstructError: # Should never occur print(f"Failed to build request update message") return False # Sends the message updater.send_broadcast(version_update_message) # After the update is announced, the service will send it to the clients, when they ask for it print(f"Broadcast was sent with version {update_version} !") return True
def send_server_information(ip, port, spread=True): # Validates the ip or domain (syntax only) if validators.domain(ip) is not True: if validators.ip_address.ipv4(ip) is not True: if validators.ip_address.ipv6(ip) is not True: print( f"Failed to validate ip or domain: {ip}. Check for typing mistakes." ) return False # Validates port number if validators.between(port, min=1, max=65535) is not True: print(f"Invalid port number: {port} is not between 1 and 65535") return False if not registry.exists(settings.ADDRESS_ID_REGISTRY): print( f"Address ID was not found in the registry! (Location: {settings.ADDRESS_ID_REGISTRY})" ) return False if not registry.exists(settings.RSA_PRIVATE_REGISTRY): print( f"RSA Private Key was not found in the registry! (Location: {settings.RSA_PRIVATE_REGISTRY})" ) return False address_id = registry.get_value(settings.ADDRESS_ID_REGISTRY) # Creates server message server_update_dict = dict( type=MessageType.SERVER_UPDATE, signature=0, address_id=address_id + 1, # Increases the address id, to indicate the information is new and up to date. address_size=len(ip), address=ip.encode("ascii"), port=port, spread=spread) # Calculate signature of message try: server_update_message = messages.SERVER_UPDATE_MESSAGE.build( server_update_dict) # Update signature server_update_dict["signature"] = messages.sign_message( server_update_message) server_update_message = messages.SERVER_UPDATE_MESSAGE.build( server_update_dict) except construct.ConstructError as e: # Should never occur print(f"Failed to build server update message: {e.args}") return False # Sends server information update (should also update local server information) updater.send_broadcast(server_update_message) print("Server information was sent to services!") return True
def setup_listener(self): port = registry.get_value(settings.PORT_REGISTRY) try: self.management_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.management_socket.bind(("0.0.0.0", port)) except socket.error as e: logging.critical(f"Failed to bind service socket to port {port}.", exc_info=True) raise e
def show_rsa_keys(): print("RSA Modulo (n):") if registry.exists(settings.RSA_MODULO_REGISTRY): print(registry.get_value(settings.RSA_MODULO_REGISTRY)) else: print("Not available.") print("") print("RSA Public Key (e):") if registry.exists(settings.RSA_PUBLIC_REGISTRY): print(registry.get_value(settings.RSA_PUBLIC_REGISTRY)) else: print("Not available.") print("") print("RSA Private Key (d):") if registry.exists(settings.RSA_PRIVATE_REGISTRY): print(registry.get_value(settings.RSA_PRIVATE_REGISTRY)) else: print("Not available.") return True
def load_settings(): global __values__ if "SETTINGS_REGISTRY" in __values__ and registry.exists( __values__["SETTINGS_REGISTRY"]): settings_path = registry.get_value(__values__["SETTINGS_REGISTRY"]) else: settings_path = __values__[ "SETTINGS_PATH"] if "SETTINGS_PATH" in __values__ else INITIAL_SETTINGS_PATH try: with open(settings_path, "r") as settings_file: data = settings_file.read() __values__.update(json.loads(data)) except FileNotFoundError: # If settings file is not found, use default settings pass
def save_settings(): global __values__ if "SETTINGS_REGISTRY" in __values__ and registry.exists( __values__["SETTINGS_REGISTRY"]): settings_path = registry.get_value(__values__["SETTINGS_REGISTRY"]) else: settings_path = __values__[ "SETTINGS_PATH"] if "SETTINGS_PATH" in __values__ else INITIAL_SETTINGS_PATH try: with open(settings_path, "w") as settings_file: data = json.dumps(__values__) settings_file.write(data) except PermissionError: logging.critical( f"Failed to write settings file due to PermissionError: {__values__['SETTINGS_PATH']}", exc_info=True)
def send_broadcast(message, sender=None): broadcaster = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) broadcaster.settimeout(settings.CONNECTION_TIMEOUT) port = registry.get_value(settings.PORT_REGISTRY) try: # Enable broadcasting mode broadcaster.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # Sends the broadcast for broadcast_address in get_all_broadcast_address(sender): logging.info(f"Sending broadcast to {broadcast_address}") broadcaster.sendto(message, (broadcast_address, port)) except socket.error: logging.error("Unknown error while sending broadcast :(", exc_info=True) finally: broadcaster.close()
def handle_server_update(self): if len(self.message) != messages.SERVER_UPDATE_MESSAGE.sizeof(): # The message has an incorrect size... logging.warning(f"Incorrect message size: {len(self.message)}") return # Parse the message try: message = messages.SERVER_UPDATE_MESSAGE.parse(self.message) except construct.ConstructError: # Should never occur logging.critical( f"Failed to parse server update message: {self.message.hex()}", exc_info=True) return # Check the running id is more updated than the current id current_id = registry.get_value(settings.ADDRESS_ID_REGISTRY) if current_id >= message.address_id: # An outdated message, should ignore... logging.info( f"Received an outdated server update message. current id: {current_id}, received id: {message.address_id}." ) return # Checks if the sender requested to spread this message using broadcast if message.spread: self.broadcast_message() # The message is updated, update our data! address = message.address.decode("ascii") registry.set_value(settings.UPDATING_SERVER_REGISTRY, address) registry.set_value(settings.PORT_REGISTRY, message.port) registry.set_value(settings.ADDRESS_ID_REGISTRY, message.address_id) logging.info( f"Updated address to {address} and port to {message.port}") # Since port could have changed, we restart our socket self.cleanup_listener() self.setup_listener()
def check_for_update(window): installed_version = updater.Version.get_installed_version() update_version = updater.Version.get_current_version() if installed_version >= update_version: return # Checks if the update should be installed automatically automatic_installation = registry.get_value( settings.AUTO_INSTALLATIONS_REGISTRY) if automatic_installation == 0: # Asks the user if they would like to update result = sg.popup_yes_no( f"A new update is available. Would you like to install it?\n\nInstalled:\t {installed_version}\nUpdate: \t{update_version}", title="Update") if result != "Yes": return # Installs the update install_update(silent=False) update_layout(window)
def cleanup_old_updates(): current_version_registry = updater.Version.get_current_version( ).get_update_registry_path() all_sub_values = registry.get_all_sub_values(settings.REGISTRY_PATH) update_prefix = settings.UPDATE_REGISTRY_FORMAT.format("") all_sub_values = [ settings.REGISTRY_PATH + "\\" + v for v in all_sub_values ] all_sub_values = [ v for v in all_sub_values if v.startswith(update_prefix) and v != current_version_registry ] for update_registry in all_sub_values: file = registry.get_value(update_registry) try: os.remove(file) except PermissionError: # File is in use, don't delete registry if file wasn't deleted as well continue except FileNotFoundError: # File appears to be missing, should delete the registry... pass registry.delete(update_registry)
def get_current_version(): major = registry.get_value(settings.UPDATE_MAJOR_REGISTRY) minor = registry.get_value(settings.UPDATE_MINOR_REGISTRY) return Version(major, minor)
def init_settings(save=True, load=True): global __values__ # Default settings that are needed before load __values__.setdefault( "REGISTRY_PATH", rf"Software\{__values__['SOFTWARE_NAME']}\{__values__['UPDATER_NAME']}" ) __values__.setdefault("SETTINGS_REGISTRY", rf"{__values__['REGISTRY_PATH']}\settings") if registry.exists(__values__['SETTINGS_REGISTRY']): settings_path = registry.get_value(__values__['SETTINGS_REGISTRY']) software_path = os.path.dirname(os.path.dirname(settings_path)) __values__['SOFTWARE_PATH'] = software_path # Load existing settings if load: load_settings() # Setting default settings values, if they don't appear in the settings.json file already __values__.setdefault( "SOFTWARE_PATH", rf"{PROGRAM_FILES}{os.path.sep}{__values__['SOFTWARE_NAME']}") __values__.setdefault( "UPDATER_PATH", rf"{__values__['SOFTWARE_PATH']}{os.path.sep}{__values__['UPDATER_NAME']}" ) __values__.setdefault( "PROGRAM_PATH", rf"{__values__['SOFTWARE_PATH']}{os.path.sep}{__values__['SOFTWARE_NAME']}" ) __values__.setdefault( "LAUNCHER_PATH", rf"{__values__['SOFTWARE_PATH']}{os.path.sep}{__values__['LAUNCHER_NAME']}" ) __values__.setdefault( "SETTINGS_PATH", rf"{__values__['UPDATER_PATH']}{os.path.sep}settings.json") __values__.setdefault( "LOGGER_PATH", rf"{__values__['UPDATER_PATH']}{os.path.sep}log.txt") __values__.setdefault( "UPDATE_PATH", rf"{__values__['UPDATER_PATH']}{os.path.sep}update.zip") __values__.setdefault("AUTO_INSTALLATIONS_REGISTRY", rf"{__values__['REGISTRY_PATH']}\auto_update") __values__.setdefault("UPDATING_SERVER_REGISTRY", rf"{__values__['REGISTRY_PATH']}\update_server") __values__.setdefault("PORT_REGISTRY", rf"{__values__['REGISTRY_PATH']}\port") __values__.setdefault("RSA_MODULO_REGISTRY", rf"{__values__['REGISTRY_PATH']}\rsa_modulo") __values__.setdefault("RSA_PUBLIC_REGISTRY", rf"{__values__['REGISTRY_PATH']}\rsa_public") __values__.setdefault("RSA_PRIVATE_REGISTRY", rf"{__values__['REGISTRY_PATH']}\rsa_private") __values__.setdefault("UPDATE_MAJOR_REGISTRY", rf"{__values__['REGISTRY_PATH']}\update_major") __values__.setdefault("UPDATE_MINOR_REGISTRY", rf"{__values__['REGISTRY_PATH']}\update_minor") __values__.setdefault("VERSION_MAJOR_REGISTRY", rf"{__values__['REGISTRY_PATH']}\version_major") __values__.setdefault("VERSION_MINOR_REGISTRY", rf"{__values__['REGISTRY_PATH']}\version_minor") __values__.setdefault("ADDRESS_ID_REGISTRY", rf"{__values__['REGISTRY_PATH']}\address_id") __values__.setdefault("UPDATE_REGISTRY_FORMAT", rf"{__values__['REGISTRY_PATH']}\Update_{{}}") # Saves the settings, if they should be saved if save: save_settings()
def get_installed_version(): major = registry.get_value(settings.VERSION_MAJOR_REGISTRY) minor = registry.get_value(settings.VERSION_MINOR_REGISTRY) return Version(major, minor)
def send_version_update(message, requester): if len(message) != messages.REQUEST_VERSION_MESSAGE.sizeof(): # The message has an incorrect size... logging.warning(f"Incorrect message size: {len(message)}") return # Parse the message try: message = messages.REQUEST_VERSION_MESSAGE.parse(message) except construct.ConstructError: # Should never occur logging.critical( f"Failed to parse request update message: {message.hex()}", exc_info=True) return # Check if the version file exists requested_version = Version(message.major, message.minor) version_registry = requested_version.get_update_registry_path() if not registry.exists(version_registry): # Version not exists... abort logging.info( f"Version {requested_version} was requested but wasn't found in the registry" ) return # Retrieve the update filepath version_filepath = registry.get_value(version_registry) try: update_file = open(version_filepath, "rb") except OSError: logging.error(f"Unable to open version file: {version_filepath}", exc_info=True) return logging.info(f"Sending update of version {requested_version}") sender = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: # Connect to the TCP server of the receiver sender.settimeout(settings.CONNECTION_TIMEOUT) sender.connect((requester[0], message.listening_port)) # Send the update file with update_file: chunk = update_file.read(settings.VERSION_CHUNK_SIZE) while len(chunk) != 0: sender.send(chunk) chunk = update_file.read(settings.VERSION_CHUNK_SIZE) logging.info( f"Finished sending update of version {requested_version}") except socket.timeout: # Connection was timed-out, too bad... abort logging.info("Connection timed out") except socket.error: # Socket error logging.info("Socket error has occurred") finally: sender.shutdown(socket.SHUT_WR) sender.close()
def download_update(self, message): listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: # Creates the TCP server that receives the update listener.settimeout(settings.CONNECTION_TIMEOUT) listener.bind(("0.0.0.0", 0)) # Bind to random port port = listener.getsockname()[1] listener.listen(1) # Build the request version message requested_version = Version(message.major, message.minor) request_version_dict = dict(type=MessageType.REQUEST_VERSION, crc32=0, listening_port=port, major=requested_version.major, minor=requested_version.minor) try: request_version_message = messages.REQUEST_VERSION_MESSAGE.build( request_version_dict) # Update CRC32 request_version_dict["crc32"] = messages.calculate_crc( request_version_message) request_version_message = messages.REQUEST_VERSION_MESSAGE.build( request_version_dict) except construct.ConstructError: # Should never occur logging.critical(f"Failed to build request update message", exc_info=True) return False # Creating the file for the update update_filepath = settings.UPDATE_PATH update_filepath = f"{update_filepath}.{requested_version}" update_file = open(update_filepath, "wb") # Request the update (so that the Updater will connect to our listening socket) port = registry.get_value(settings.PORT_REGISTRY) temp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) temp_socket.sendto(request_version_message, 0, (self.sender[0], port)) temp_socket.shutdown(2) temp_socket.close() # Awaits for sender to connect receiver, _ = listener.accept() receiver.settimeout(settings.CONNECTION_TIMEOUT) data_received = 0 hash_object = settings.HASH_MODULE() # Download the update logging.info(f"Downloading update of version {requested_version}") with update_file: while data_received < message.size: chunk = receiver.recv(settings.VERSION_CHUNK_SIZE) if len( chunk ) == 0: # Occurs when the other side closed the conenction break update_file.write(chunk) hash_object.update(chunk) data_received += len(chunk) # Close the TCP connection receiver.shutdown(socket.SHUT_RD) receiver.close() # Validate the update signature if not rsa_signing.validate_hash(hash_object, message.update_signature): # Delete this invalid update file os.remove(update_filepath) logging.info( "Invalid signature for update file (maybe tampered?)") return False # Update the registry with the current update version_registry = requested_version.get_update_registry_path() registry.set_value(version_registry, os.path.abspath(update_filepath)) requested_version.update_current_version() logging.info(f"Received new update: version {requested_version}") except socket.timeout: # Connection was timed-out, too bad... abort logging.info("Connection timed out") return False except socket.error: # Socket error logging.info("Socket error has occurred") return False finally: listener.close() return True