def __init__(self): self.logger = Logger() self.app_path = sys.path[0] self.current_module = None self.current_module_name = None self.dispatcher = Dispatcher() self.modules = self._list_modules() self.modules_history = [] self.config = load_config() self.completer_nested_dict = self._get_dict_completion() self.console_completer = NestedCompleter.from_nested_dict( self.completer_nested_dict) self.global_options = {} self.prompt_style = Style.from_dict({ # User input (default text), no value = system default. '': self.config['THEME']['user_input'], # Prompt. 'base': self.config['THEME']['base'], 'pound': self.config['THEME']['pound'], 'module': self.config['THEME']['module'], 'category': self.config['THEME']['category'], }) self.prompt = [ ('class:base', '[hbf] '), ('class:module', ''), ('class:category', ''), ('class:pound', '> '), ]
def __init__(self, hbf_config): self.name = None self.meta = { 'name': '', 'version': '', 'description': '', 'author': '' } self.logger = Logger() self.options = [] self.config = hbf_config
class Validator: """ Class allowing to check module options (if set and if format is ok). """ def __init__(self): self.logger = Logger() def _args_validator(self, options_dict): """ Check arguments type validity & Convert to the specified format. :param options_dict: module options dictionary :return: Bool """ for option in options_dict: try: if option["Type"] == "int": if not isinstance(option["Value"], int): option["Value"] = int(option["Value"], 10) if option["Type"] == "bool": if not isinstance(option["Value"], bool): if str(option["Value"]).upper() == "FALSE": option["Value"] = False elif str(option["Value"]).upper() == "TRUE": option["Value"] = True else: raise ValueError if option["Value"] == "None": option["Value"] = None except ValueError: self.logger.handle( "Value error: {} is not a member of {}".format( option["Name"], option["Type"])) return False return True def check_args(self, options_dict): """ Check if all arguments are defined by user, or set default value if available. :param options_dict: module options dictionary :return: Bool """ if len(options_dict) > 0: for option in options_dict: if option["Required"] and option["Value"] == "": if option["Default"] == "": self.logger.handle( "OptionValidateError: The following options failed to validate: {}." .format(option["Name"]), Logger.ERROR) return False else: option["Value"] = option["Default"] if not self._args_validator(options_dict): return False return True
def hb_connect(device, baudrate, timeout): """ Connect to the hydrabus device. :param device: String, hydrabus device path. :param baudrate: integer, baudrate speed to communicate with hydrabus. :param timeout: integer, read timeout value (sec). :return: serial instance. """ try: serial_instance = serial.Serial(device, baudrate, timeout=timeout) return serial_instance except serial.serialutil.SerialException as err: Logger().handle(err, Logger.ERROR) return False
def __init__(self, hbf_config): super(Baudrate, self).__init__(hbf_config) self.logger = Logger() self.vowels = ["a", "A", "e", "E", "i", "I", "o", "O", "u", "U"] self.whitespace = [" ", "\t", "\r", "\n"] self.punctation = [".", ",", ":", ";", "?", "!"] self.control = [b'\x0e', b'\x0f', b'\xe0', b'\xfe', b'\xc0'] self.baudrates = [9600, 19200, 38400, 57600, 115200] self.hb_serial = None self.meta.update({ 'name': 'UART baudrate detection', 'version': '0.0.1', 'description': 'Automatically detect baudrate of a target device', 'author': 'Jordan Ovrè' }) self.options = [ {"Name": "hydrabus", "Value": "", "Required": True, "Type": "string", "Description": "Hydrabus device", "Default": self.config["HYDRABUS"]["port"]}, {"Name": "timeout", "Value": "", "Required": True, "Type": "int", "Description": "Hydrabus read timeout", "Default": self.config["HYDRABUS"]["read_timeout"]}, {"Name": "trigger", "Value": "", "Required": True, "Type": "bool", "Description": "If true, trigger the device if hydrabus didn't receive anything from the target", "Default": False} ]
def hb_wait_ubtn(serial_instance): """ Loop until user press hydrabus UBTN. :param serial_instance: hydrabus serial instance. :return: Nothing. """ # timeout=1 minute timeout = time.time() + 60 * 1 try: while True: if serial_instance.hydrabus.read(1) == 'B'.encode('utf-8'): if serial_instance.hydrabus.read(3) == 'BIO'.encode('utf-8'): # carriage return needed to reset interface serial_instance.hydrabus.write(b'\x0D\x0A') time.sleep(0.2) serial_instance.read(serial_instance.hydrabus.in_waiting) break if time.time() > timeout: Logger().handle("Wait UBTN timeout reached", Logger.ERROR) break except KeyboardInterrupt: pass
class Baudrate(AModule): """ Iterate baudrate to find the correct value TODO: Add Parity bit and Stop bit option """ def __init__(self, hbf_config): super(Baudrate, self).__init__(hbf_config) self.logger = Logger() self.vowels = ["a", "A", "e", "E", "i", "I", "o", "O", "u", "U"] self.whitespace = [" ", "\t", "\r", "\n"] self.punctation = [".", ",", ":", ";", "?", "!"] self.control = [b'\x0e', b'\x0f', b'\xe0', b'\xfe', b'\xc0'] self.baudrates = [9600, 19200, 38400, 57600, 115200] self.hb_serial = None self.meta.update({ 'name': 'UART baudrate detection', 'version': '0.0.1', 'description': 'Automatically detect baudrate of a target device', 'author': 'Jordan Ovrè' }) self.options = [ {"Name": "hydrabus", "Value": "", "Required": True, "Type": "string", "Description": "Hydrabus device", "Default": self.config["HYDRABUS"]["port"]}, {"Name": "timeout", "Value": "", "Required": True, "Type": "int", "Description": "Hydrabus read timeout", "Default": self.config["HYDRABUS"]["read_timeout"]}, {"Name": "trigger", "Value": "", "Required": True, "Type": "bool", "Description": "If true, trigger the device if hydrabus didn't receive anything from the target", "Default": False} ] def gen_char_list(self): """ Generate human readable characters list. :return: character's list """ c = ' ' valid_characters = [] while c <= '~': valid_characters.append(c) c = chr(ord(c) + 1) for c in self.whitespace: if c not in valid_characters: valid_characters.append(c) for c in self.control: if c not in valid_characters: valid_characters.append(c) return valid_characters def change_baudrate(self, baudrate): """ This function change the baudrate speed for the target device. It is necessary to stop echo UART RX to read the return of the change baudrate function from Hydrabus. :param baudrate: Baudrate speed dictionary (decimal and hexadecimal value) :return: bool """ self.init_hb() self.hb_serial.baud = baudrate if self.hb_serial.baud != baudrate: self.logger.handle(f'Unable to switch to baudrate {baudrate}', Logger.ERROR) return False else: # Starting binary UART bridge mode self.logger.handle(f'switching to baudrate {baudrate}...', Logger.INFO) self.logger.handle("Starting BBIO_UART_BRIDGE", Logger.INFO) self.hb_serial.bridge() return True def trigger_device(self): """ Send a carriage return to trigger the target device in case no byte(s) is received """ self.logger.handle("Trigger the device", Logger.INFO) self.hb_serial.write(b'\x0D\x0A') # Read back \r\n characters self.hb_serial.read(2) time.sleep(0.5) def baudrate_detect(self): """ The main function. Change the baudrate speed and check if the received byte(s) from RX pin are valid characters. 25 valid characters are required to identify the correct baudrate value. """ count = 0 whitespace = 0 punctuation = 0 vowels = 0 threshold = 25 valid_characters = self.gen_char_list() try: trigger = self.get_option_value("trigger") except UserWarning: self.logger.handle("Unable to recover trigger settings, set it to False", Logger.ERROR) trigger = False self.logger.handle("Starting baurate detection, turn on your serial device now", Logger.HEADER) self.logger.handle("Press Ctrl+C to cancel", Logger.HEADER) for baudrate in self.baudrates: loop = 0 if self.change_baudrate(baudrate): progress = self.logger.progress('Read byte') while True: tmp = self.hb_serial.read(1) if len(tmp) > 0: # Dynamic print try: tmp.decode() progress.status(tmp.decode()) except UnicodeDecodeError: tmp2 = tmp progress.status('0x{}'.format(codecs.encode(tmp2, 'hex').decode())) try: byte = tmp.decode('utf-8') except UnicodeDecodeError: byte = tmp if byte in valid_characters: if byte in self.whitespace: whitespace += 1 elif byte in self.punctation: punctuation += 1 elif byte in self.vowels: vowels += 1 count += 1 else: whitespace = 0 punctuation = 0 vowels = 0 count = 0 progress.stop() self.logger.handle("Please press hydrabus ubtn in order to switch baudrate speed", Logger.USER_INTERACT) hb_wait_ubtn(self.hb_serial) break if count >= threshold and whitespace > 0 and punctuation >= 0 and vowels > 0: progress.stop() self.logger.handle("Valid Baudrate found: {}".format(baudrate), Logger.RESULT) resp = prompt('Would you like to open a miniterm session ? N/y: ') if resp.upper() == 'Y': self.hb_serial.hydrabus.close() config = load_config() config['HYDRABUS']['port'] = self.get_option_value("hydrabus") miniterm(config=config) # Run init_hb again to properly reset and close the hydrabus session self.init_hb() self.logger.handle("Please press hydrabus ubtn in order to return BBIO mode", Logger.USER_INTERACT) hb_wait_ubtn(self.hb_serial) break break elif trigger and loop < 3: loop += 1 self.trigger_device() continue else: progress.stop() self.logger.handle("Please press hydrabus ubtn in order to switch baudrate speed", Logger.USER_INTERACT) hb_wait_ubtn(self.hb_serial) break else: self.logger.handle("Please press hydrabus ubtn in order to switch baudrate speed", Logger.USER_INTERACT) hb_wait_ubtn(self.hb_serial) break def init_hb(self): try: device = self.get_option_value("hydrabus") timeout = int(self.get_option_value("timeout")) self.hb_serial = UART(device) self.hb_serial.timeout = timeout return True except serial.SerialException as err: self.logger.handle("{}".format(err), Logger.ERROR) return False def run(self): if self.init_hb(): self.baudrate_detect() self.logger.handle("Reset hydrabus to console mode", Logger.INFO) self.hb_serial.hydrabus.exit_bbio() self.hb_serial.hydrabus.close() else: self.logger.handle("Unable to init hydrabus in UART mode, please try the 'reset' command", Logger.ERROR)
class HydraFramework: """ Framework core engine """ def __init__(self): self.logger = Logger() self.app_path = sys.path[0] self.current_module = None self.current_module_name = None self.dispatcher = Dispatcher() self.modules = self._list_modules() self.modules_history = [] self.config = load_config() self.completer_nested_dict = self._get_dict_completion() self.console_completer = NestedCompleter.from_nested_dict( self.completer_nested_dict) self.global_options = {} self.prompt_style = Style.from_dict({ # User input (default text), no value = system default. '': self.config['THEME']['user_input'], # Prompt. 'base': self.config['THEME']['base'], 'pound': self.config['THEME']['pound'], 'module': self.config['THEME']['module'], 'category': self.config['THEME']['category'], }) self.prompt = [ ('class:base', '[hbf] '), ('class:module', ''), ('class:category', ''), ('class:pound', '> '), ] def update_prompt(self): """ Update the user prompt. """ if self.current_module_name is not None: category = self.current_module_name.split("/")[0] module = self.current_module_name.split("/")[1] self.prompt = [ ('class:base', '[hbf] '), ('class:category', '{}'.format(category)), ('class:module', '({})'.format(module)), ('class:pound', '> '), ] else: self.prompt = [ ('class:base', '[hbf] '), ('class:category', ''), ('class:module', ''), ('class:pound', '> '), ] def _get_dict_completion(self): """ Get all command with associated arguments and return a dict for NestedCompleter. nested_dict = { 'set': { 'hydrabus': None, ... }, 'setc': { { 'section1': { {key1: None}, {key2: None} } } 'exit': None 'use': { {'module1': None}, ... } } :return: nested dictionary """ nested_completion_dict = {} modules_dict = {} config_dict = {} # Get all based command for command in self.dispatcher.commands: if len(command["arguments"]) == 0: nested_completion_dict.update({command["name"]: None}) else: nested_completion_dict.update( {command["name"]: command["arguments"]}) # Append all loaded module to use command for module in self.modules: modules_dict.update({module["path"]: None}) nested_completion_dict["use"] = modules_dict # Append all config section and key to setc command for section in self.config: if section != "DEFAULT": config_key_dict = {} config_dict.update({section: None}) if len(self.config[section]) > 0: for key in self.config[section]: config_key_dict.update({key: None}) config_dict[section] = config_key_dict nested_completion_dict["setc"] = config_dict return nested_completion_dict def update_completer_options_list(self): """ Update prompt completer options list when loading a new module. :return: Nothing """ options = {} if isinstance(self.current_module, AModule): for option in self.current_module.options: options.update({option["Name"]: None}) self.completer_nested_dict["set"] = options self.completer_nested_dict["unset"] = options self.console_completer = NestedCompleter.from_nested_dict( self.completer_nested_dict) def update_completer_global_options_list(self): """ Update prompt completer global options list when loading a new module. :return: Nothing """ options = {} for keys, _ in self.global_options.items(): options.update({keys: None}) self.completer_nested_dict["setg"] = options self.completer_nested_dict["unsetg"] = options self.console_completer = NestedCompleter.from_nested_dict( self.completer_nested_dict) def _list_modules(self): """ Generate modules path and attributes list. :return: List of available modules """ modules = [] module_name = "hbfmodules" try: package = import_module(module_name) except ImportError: self.logger.handle( "Unable to find any modules, please run 'hbfupdate' " "script to install available modules...", Logger.ERROR) else: for loader, module, is_pkg in pkgutil.walk_packages( package.__path__, prefix=package.__name__ + '.'): try: imported_module = import_module(module) for x in dir(imported_module): obj = getattr(imported_module, x) if inspect.isclass(obj) and issubclass( obj, AModule) and obj is not AModule: module_path = module.replace('hbfmodules.', '').replace('.', '/') modules.append({"path": module_path, "class": obj}) except ImportError: self.logger.handle( 'Error dynamically import package "{}"...'.format( module), Logger.ERROR) self.logger.handle( "{} modules loaded, run 'hbfupdate' command to install the latest modules" .format(len(modules)), Logger.USER_INTERACT) return modules def run(self, file_script=None): """ Main loop, waiting for user input. :return: """ session = PromptSession() # This parts is used to automate test by passing file script if file_script is not None: file_script = Path(file_script) if file_script.is_file(): with file_script.open() as f: commands = f.readlines() for command in commands: command = session.prompt(self.prompt, style=self.prompt_style, default=command.strip(), accept_default=True) self.dispatcher.handle(self, command) self.update_prompt() else: self.logger.handle("File does not exist or it is not a file", self.logger.ERROR) # Normal mode waiting for user_input while True: try: command = session.prompt(self.prompt, style=self.prompt_style, completer=self.console_completer, complete_while_typing=False) self.dispatcher.handle(self, command) self.update_prompt() except KeyboardInterrupt: self.logger.handle( "Please use 'exit' command or ctrl+D key to properly quit the framework", Logger.INFO) except EOFError: exit(0)
def reset_hb(hbf_instance): """ Return hydrabus into console mode. :param hbf_instance: Hydrabus framework instance (self). :return: Nothing. """ logger = Logger() hydrabus_cfg = hbf_instance.config['HYDRABUS'] if hydrabus_cfg['port'] is None or hydrabus_cfg['port'] == '-': logger.handle('port is not set on the configuration (setc command)', Logger.ERROR) else: try: serial_instance = hb_connect(device=hydrabus_cfg['port'], baudrate=115200, timeout=1) except serial.SerialException as e: logger.handle( "could not open port {!r}: {}".format(hydrabus_cfg['port'], e), logger.ERROR) return except UserWarning as err: logger.handle("{}".format(err), Logger.ERROR) return if isinstance(serial_instance, serial.Serial): hb_reset(serial_instance) hb_close(serial_instance) logger.handle("Reset sequence successfully sent to Hydrabus...", Logger.SUCCESS) else: logger.handle( "Unable to reset Hydrabus due to connection error...", Logger.ERROR)
def __init__(self): self.github_base_url = 'https://api.github.com' self.logger = Logger() self.not_updated = [] self.to_update = [] self.to_install = []
class HBFUpdate: def __init__(self): self.github_base_url = 'https://api.github.com' self.logger = Logger() self.not_updated = [] self.to_update = [] self.to_install = [] def _get_installed_modules(self): """ Return a dict of currently installed module(s). :return: A dict of currently installed module(s) {'module_name': 'version', ...}. """ module_name = "hbfmodules" installed_modules = {} try: package = import_module(module_name) except ImportError: self.logger.handle('No modules currently installed', Logger.ERROR) return installed_modules for loader, module, is_pkg in pkgutil.walk_packages( package.__path__, prefix=package.__name__ + '.'): try: imported_module = import_module(module) for x in dir(imported_module): obj = getattr(imported_module, x) if inspect.isclass(obj) and issubclass( obj, AModule) and obj is not AModule: installed_modules[ module] = pkg_resources.get_distribution( module).version except ImportError: self.logger.handle( 'Error dynamically import package "{}"... Unable to update it' .format(module), Logger.ERROR) self.not_updated.append(module) return installed_modules def _get_available_modules(self): """ Return a dict of module that have a release on the hydrabus-framework organization. :return: A dict of available module {'module_name': 'version', ...}. """ invalids = ['hbfmodules.skeleton', 'framework'] modules = {} resp = requests.get('{}{}'.format(self.github_base_url, '/orgs/hydrabus-framework/repos')) if resp.status_code == 200: for pkg in resp.json(): if pkg["name"] not in invalids: module_release_url = self.github_base_url +\ '/repos/hydrabus-framework/{}/releases/latest'.format(pkg["name"]) resp = requests.get(module_release_url) if resp.status_code == 200: modules[pkg["name"]] = resp.json()['tag_name'] return modules def _get_latest_framework_version(self): """ Return the latest release version of the Hydrabus framework. :return: string. """ module_release_url = self.github_base_url + '/repos/hydrabus-framework/framework/releases/latest' resp = requests.get(module_release_url) if resp.status_code == 200: return resp.json()['tag_name'] else: self.logger.handle( 'Unable to get the latest framework released version', Logger.ERROR) return None def _get_latest_release_url(self, module_name): """ Get the latest release URL of a module or framework. :param module_name: module name. :return: url or None if not found. """ module_release_url = self.github_base_url + '/repos/hydrabus-framework/{}/releases/latest'.format( module_name) resp = requests.get(module_release_url) if resp.status_code == 200: return resp.json()["tarball_url"] else: self.logger.handle( f"Unable to retrieve latest release URL for module '{module_name}'", Logger.ERROR) return None @staticmethod def _get_filename_from_cd(cd): """ Get filename from content-disposition. :param cd: Content-Disposition HTTP header. :return: filename from Content-Disposition or None """ if not cd: return None fname = re.findall('filename=(.+)', cd) if len(fname) == 0: return None return fname[0] def _download_release(self, module_tarball_url, package_name): """ Download the latest release of a module or framework. :param module_tarball_url: release URL. :param package_name: The package name downloaded (module or framework). :return: filename or None. """ self.logger.handle("Downloading {}...".format(package_name)) resp = requests.get(module_tarball_url, stream=True) if resp.status_code == 200: filename = '/tmp/hbfmodules/{}'.format( self._get_filename_from_cd( resp.headers.get('content-disposition'))) if not os.path.exists(os.path.dirname(filename)): try: os.makedirs(os.path.dirname(filename)) except OSError as exc: if exc.errno != errno.EEXIST: raise open(filename, 'wb').write(resp.content) return filename return None @staticmethod def _extract_tarball(filename): """ Extract the specified tarball. :param filename: the tarball file path :return: Directory path of the extracted archive """ tar = tarfile.open(filename) path = re.sub(r'\.tar.gz$', '', filename) setup_dir = '{}/{}'.format(path, tar.getmembers()[0].name) tar.extractall(path) tar.close() return setup_dir def _manage_install(self, package_name): """ Manage the installation of the specified package (module or framework). :param package_name: The package name to install (module or framework). :return: Bool: True if successfully update, False otherwise. """ release_tarball_url = self._get_latest_release_url(package_name) if release_tarball_url: filename = self._download_release(release_tarball_url, package_name) if filename: setup_dir = self._extract_tarball(filename) try: return_code = subprocess.call( ['python', 'setup.py', 'install'], cwd=setup_dir, stdout=subprocess.DEVNULL) if return_code != 0: return False else: self.logger.handle( "'{}' successfully installed".format(package_name), Logger.SUCCESS) return True except: self.logger.handle( "The setup command are failed for the '{}' module". format(package_name), Logger.ERROR) traceback.print_exc() return False else: self.logger.handle( "Failed to download the latest release for the module '{}'" .format(package_name), Logger.ERROR) return False else: return False def update(self, update_framework=None): """ This script check all released Hydrabus Framework modules and compare it with currently installed modules. If an update is available, this script install it. Moreover, if a module is available and not installed, it will be installed. :param update_framework: if True check if a framework update is available. :return: Nothing """ if update_framework: latest_release_version = self._get_latest_framework_version() if latest_release_version: if latest_release_version > pkg_resources.get_distribution( 'hydrabus_framework').version: self.logger.handle( 'A new framework release is available, running update...', Logger.INFO) if not self._manage_install('framework'): self.not_updated.append('framework') else: self.logger.handle('Hydrabus framework is up-to-date', Logger.SUCCESS) else: self.not_updated.append('framework') available_modules = self._get_available_modules() installed_modules = self._get_installed_modules() for module, version in available_modules.items(): installed_module_version = installed_modules.get(module, None) if installed_module_version is not None: if installed_module_version < version: self.to_update.append(module) else: self.to_install.append(module) for module_name in self.to_update: self.logger.handle("Update module '{}'".format(module_name), Logger.INFO) if not self._manage_install(module_name): self.not_updated.append(module_name) for module_name in self.to_install: self.logger.handle("Install module '{}'".format(module_name), Logger.INFO) if not self._manage_install(module_name): self.not_updated.append(module_name) if len(self.not_updated) > 0: self.logger.handle( "Unable to update/install the following package:", Logger.ERROR) for module in self.not_updated: print(" - {}".format(module))
class AModule(ABC): def __init__(self, hbf_config): self.name = None self.meta = { 'name': '', 'version': '', 'description': '', 'author': '' } self.logger = Logger() self.options = [] self.config = hbf_config def __name__(self): """ Simply return the module name :return: module name """ return self.name def get_option_value(self, option_name): """ Return the value of a specific option. :param option_name: The needed option name. :return: Value. """ for option in self.options: if option["Name"] == option_name: return option["Value"] else: raise UserWarning( "Value {} not found in module options".format(option_name)) def show_options(self): """ Print available options for the module to user console. :return: Nothing. """ formatted_options = [] if len(self.options) > 0: self.print_meta() for option in self.options: if option["Default"] != "" and option["Value"] == "": formatted_options.append({ 'Name': option["Name"], 'Value': option["Default"], 'Required': option["Required"], 'Description': option["Description"] }) else: formatted_options.append({ 'Name': option["Name"], 'Value': option["Value"], 'Required': option["Required"], 'Description': option["Description"] }) self.logger.print_tabulate(formatted_options, headers={ "Name": "Name", "Description": "Description", "Value": "Value", "Required": "Required" }) def print_meta(self): """ Print meta of the module (author, module name, description). :return: Nothing. """ self.logger.handle('Author: {}'.format(self.meta['author']), Logger.HEADER) self.logger.handle( 'Module name: {}, version {}'.format(self.meta['name'], self.meta['version']), Logger.HEADER) self.logger.handle('Description: {}'.format(self.meta['description']), Logger.HEADER) @abstractmethod def run(self): """ Main function. :return: """ pass
def __init__(self): self.logger = Logger()