from scrounger.utils.config import Log as _Log from scrounger.utils.config import binary_memory as _memory """ Module with utility functions. """ def execute(command): """ Executes a command on the local host. :param str command: the command to be executed :return: returns the output of the STDOUT or STDERR """ from subprocess import check_output, STDOUT command = "{}; exit 0".format(command) # log command that is going to be run _Log.debug("Shell Command: {}".format(command)) return check_output(command, stderr=STDOUT, shell=True) def process(command): """ Executes a command and returns the process :param command: the command to be executed :return: returns the process off the executed command """ from subprocess import Popen, PIPE return Popen(command, stdout=PIPE, stderr=PIPE, shell=True) def file_exists(file_path): """ Checks if a file exists in the local host. :param str file_path: the path to the local file to check :return: True if the file exists or False otherwise """ from os import path return path.isfile(file_path) def remove_multiple_spaces(string): """ Strips and removes multiple spaces in a string. :param str string: the string to remove the spaces from :return: a new string without multiple spaces and stripped """ from re import sub return sub(" +", " ", string.strip()) def strings(binary): """ Runs and returns the results of the command strings on a binary file :params str binary: the path to the binary :return: the raw result of the strings command """ return execute("strings {}".format(binary)) def grep(needle, haystack, grep_options): """ Returns the result of greping for a needle in a haystack :param str needle: the needle to look for :param str haystack: the haystack to look in :param str grep_options: the modifiers to be passed to grep :return: the result of the grep command """ return execute("grep {} \"{}\" {} /dev/null".format( grep_options, needle, haystack)) def pretty_grep(needle, haystack): """ Returns a well formatted dict with the results of grepping the needle in the haystack :param str needle: the needle to look for - needs to be a regex :param str haystack: the haystack to look in :return: a dict ordered by filename with a list of dict containing the finding and the line number """ grep_result = grep(needle, haystack, "-arEin") findings = {} for line in grep_result.split("\n"): # if line is blank or if it does not have the right format if not line or line.count(":") < 2: continue filename, line_number, details = line.split(":", 2) # creat a new list if filename not in findings if filename not in findings: findings[filename] = [] findings[filename].append({ "line": line_number.strip(), "details": details.strip() }) return findings def pretty_multiline_grep(needle, haystack, no_lines, after=True): """ Returns a well formatted dict with the results of grepping the needle in the haystack :param str needle: the needle to look for - needs to be a regex :param str haystack: the haystack to look in :param int no_lines: number of lines to be displayed :param Bool after: True if lines to display are after or False if lines to display are before :return: a dict ordered by filename with a list of dict containing the finding and the line number """ additional_modifiers = "-{} {}".format("A" if after else "B", int(no_lines)) grep_result = grep(needle, haystack, "{} -arEin".format( additional_modifiers)) findings = {} for line in grep_result.split("\n"): # if line is blank or if it does not have the right format if not line or (line.count(":") < 2 and ( line.count("-") < 2 or len(line) < 5)): continue if line.count(":") < 2: filename, line_number, details = line.split("-", 2) else: filename, line_number, details = line.split(":", 2) # create a new list if filename not in findings if filename not in findings: findings[filename] = [] findings[filename].append({ "line": line_number.strip(), "details": details.strip() }) return findings def pretty_grep_to_str(grep_result, haystack, ignore=None): """ Returns a str containing the grep results without the ignored paths :param dict grep_result: the result of a pretty_grep call :param str haystack: the base path of the grrp haystack :param list ignore: a list of str with paths to be ignored :return: a pretty str with the grep results """ if not ignore: ignore = [] final_str = "" for filename in grep_result: # bypass ignored paths if any([ignore_path in filename for ignore_path in ignore]): continue final_str = "{}\n* {}".format(final_str, filename.replace(haystack, "")) # sort the results by line for result in sorted( grep_result[filename], key=lambda k: int(k["line"])): final_str = "{}\n * Line {}: {}".format(final_str, result["line"], result["details"]) return final_str # ****************************************************************************** # Interactive Process # ****************************************************************************** class InteractiveProcess(object): """ Class representing an object that interacts with a binary multiple times """ _process = _command = _executable = None def __init__(self, command): """ Creates an interactive process to interact with out of a command :param str command: the command to be executed """ from fcntl import fcntl, F_GETFL, F_SETFL from subprocess import Popen, PIPE import os self._command = command self._executable = command.split(" ", 1)[0] _Log.debug("Starting the interactive process: {}".format(command)) self._process = Popen(command, shell=True, stdout=PIPE, stdin=PIPE, stderr=PIPE) fcntl(self._process.stdin, F_SETFL, fcntl(self._process.stdin, F_GETFL) | os.O_NONBLOCK) fcntl(self._process.stdout, F_SETFL, fcntl(self._process.stdout, F_GETFL) | os.O_NONBLOCK) fcntl(self._process.stderr, F_SETFL, fcntl(self._process.stderr, F_GETFL) | os.O_NONBLOCK) def write(self, command): """ Writes a command into the interactive process :param str command: the command to be sent to the interactive process :return: nothing """ import os _Log.debug("Sending to process {}: {}".format( self._executable, command)) # add a new line and send the command to the process stdin os.write(self._process.stdin.fileno(), "{}\n".format(command)) def read(self): """ Reads from the process stdout :return: a str with the stdout result or None """ import os try: return self._process.stdout.read() except: _Log.debug("Nothing to read from {}".format(self._executable)) # there is nothing to read, return None return None def error(self): """ Reads from the process stderr :return: a str with the stderr result or None """ import os try: return self._process.stderr.read() except: _Log.debug("Nothing to read stderr {}".format(self._executable)) # there is nothing to read, return None return None def kill(self): """ Stops the process - avoid hanging process """ # try to communicate first and then kill try: self._process.communicate("") self._process.kill() except: # couldn't kill pass # ****************************************************************************** # Requires Decorator # ****************************************************************************** class OSNotSupportedException(Exception): pass class BinaryNotFoundException(Exception): KNOWN_BINARIES = { "jtool": "http://www.newosxbook.com/tools/jtool.tar", "ldid": "https://github.com/daeken/ldid", } def __init__(self, message, binary): """ Creates a binary not found exception :param str message: the message to be displayed by the error :param str binary: the binary that was not found """ super(BinaryNotFoundException, self).__init__(message) self.binary = binary class IOSBinaryNotFoundException(BinaryNotFoundException): KNOWN_IOS_BINARY_PACKAGES = { "plutil": "com.ericasadun.utilities", "appinst": "com.linusyang.appinst", } BUNDLED_IOS_BINARIES = [ "clutch", "dump_file_protection", "dump_backup_flag", "dump_keychain", "dump_log", "listapps", "gdb" ] class AndroidBinaryNotFoundException(BinaryNotFoundException): pass class UnauthorizedDevice(Exception): pass class requires_unix(object): """ Decorator that checks if the running OS is unix base """ def __call__(self, func): def wrapper(obj=None, *args, **kwargs): from scrounger.core.device import Host SUPPORTED_OS = ["linux", "darwin"] host_os = Host().os() if host_os not in SUPPORTED_OS: raise OSNotSupportedException( "{} not supported.".format(host_os)) return func() if not obj else func(obj, *args, **kwargs) return wrapper class requires_binary(object): """ Decorator that checks if the required binary is in the path """ def __init__(self, binary): """ Creates a decorator that will check if a binary is in the path :param str binary: the binary to verify """ self._binary = binary def __call__(self, func): def wrapper(obj=None, *args, **kwargs): # check if we checked and found the binary before if self._binary not in _memory["binary"]: # check if binary exists binary = execute("command -v {}".format(self._binary)) if not binary or 'not found' in binary: raise BinaryNotFoundException("{} binary not found.".format( self._binary), self._binary) # binary found - add it to the list _memory["binary"] += [self._binary] return func() if not obj else func(obj, *args, **kwargs) return wrapper class requires_ios_binary(object): """ Decorator that checks if the required binary is in the ios device path """ def __init__(self, device, binary): """ Creates a decorator that will check if a binary is in the ios PATH :param str device: the device where the binary is to be found :param str binary: the binary to verify """ self._binary = binary self._device = device def __call__(self, func): def wrapper(obj=None, *args, **kwargs): # get device id device_id = self._device.device_id() if device_id not in _memory["ios"]: _memory["ios"][device_id] = [] # check if we checked and found the binary before if self._binary not in _memory["ios"][device_id]: binary = self._device.execute( "command -v {}".format(self._binary))[0] if not binary or 'not found' in binary: raise IOSBinaryNotFoundException( "{} binary not found.".format(self._binary), self._binary) # binary found - add it to the list _memory["ios"][device_id] += [self._binary] return func() if not obj else func(obj, *args, **kwargs) return wrapper class requires_ios_package(object): """ Decorator that checks if the required packages are installed """ def __init__(self, device, package): """ Creates a decorator that will check if a packages is installed :param str device: the device where the package is to be found :param str package: the package to verify """ self._package = package self._device = device def __call__(self, func): def wrapper(obj=None, *args, **kwargs): # get device id device_id = self._device.device_id() if device_id not in _memory["ios_packages"]: _memory["ios_packages"][device_id] = [] # check if we checked and found the binary before if self._package not in _memory["ios_packages"][device_id]: packages = self._device.execute("dpkg -l")[0] if self._package not in packages: raise Exception("{} not installed.".format(self._package), self._package) # binary found - add it to the list _memory["ios_packages"][device_id] += [self._package] return func() if not obj else func(obj, *args, **kwargs) return wrapper class requires_android_binary(object): """ Decorator that checks if the required binary is in the android path """ def __init__(self, device, binary): """ Creates a decorator that will check if a binary is in the android PATH :param str device: the device where the binary is to be found :param str binary: the binary to verify """ self._binary = binary self._device = device def __call__(self, func): def wrapper(obj=None, *args, **kwargs): # get device id device_id = self._device.device_id() if device_id not in _memory["android"]: _memory["android"][device_id] = [] # check if we checked and found the binary before if self._binary not in _memory["android"][device_id]: binary = self._device.execute( "command -v {}".format(self._binary)) if not binary or 'not found' in binary: raise AndroidBinaryNotFoundException( "{} binary not found.".format(self._binary), self._binary) # binary found - add it to the list _memory["android"][device_id] += [self._binary] return func() if not obj else func(obj, *args, **kwargs) return wrapper