""" Contains helpers for interacting with the console. Includes definition for the class that is used to run the console. """ import StringIO import atexit import cmd2 import os import re import readline import string import sys import itertools from .util import PappyException from .colors import Styles, Colors, verb_color, scode_color, path_formatter, host_color from . import config from twisted.internet import defer ################### ## Helper functions def print_pappy_errors(func): def catch(*args, **kwargs): try: func(*args, **kwargs) except PappyException as e: print str(e) return catch @defer.inlineCallbacks def load_reqlist(line, allow_special=True, ids_only=False): """ load_reqlist(line, allow_special=True) A helper function for parsing a list of requests that are passed as an argument. If ``allow_special`` is True, then it will parse IDs such as ``u123`` or ``s123``. Even if allow_special is false, it will still parse ``m##`` IDs. Will print any errors with loading any of the requests and will return a list of all the requests which were successfully loaded. Returns a deferred. :Returns: Twisted deferred """ from .http import Request # Parses a comma separated list of ids and returns a list of those requests # prints any errors ids = re.split(',\s*', line) reqs = [] if not ids_only: for reqid in ids: try: req = yield Request.load_request(reqid, allow_special) reqs.append(req) except PappyException as e: print e defer.returnValue(reqs) else: defer.returnValue(ids) def print_table(coldata, rows): """ Print a table. Coldata: List of dicts with info on how to print the columns. ``name`` is the heading to give column, ``width (optional)`` maximum width before truncating. 0 for unlimited. Rows: List of tuples with the data to print """ # Get the width of each column widths = [] headers = [] for data in coldata: if 'name' in data: headers.append(data['name']) else: headers.append('') empty_headers = True for h in headers: if h != '': empty_headers = False if not empty_headers: rows = [headers] + rows for i in range(len(coldata)): col = coldata[i] if 'width' in col and col['width'] > 0: maxwidth = col['width'] else: maxwidth = 0 colwidth = 0 for row in rows: printdata = row[i] if isinstance(printdata, dict): collen = len(str(printdata['data'])) else: collen = len(str(printdata)) if collen > colwidth: colwidth = collen if maxwidth > 0 and colwidth > maxwidth: widths.append(maxwidth) else: widths.append(colwidth) # Print rows padding = 2 is_heading = not empty_headers for row in rows: if is_heading: sys.stdout.write(Styles.TABLE_HEADER) for (col, width) in zip(row, widths): if isinstance(col, dict): printstr = str(col['data']) if 'color' in col: colors = col['color'] formatter = None elif 'formatter' in col: colors = None formatter = col['formatter'] else: colors = None formatter = None else: printstr = str(col) colors = None formatter = None if len(printstr) > width: trunc_printstr=printstr[:width] trunc_printstr=trunc_printstr[:-3]+'...' else: trunc_printstr=printstr if colors is not None: sys.stdout.write(colors) sys.stdout.write(trunc_printstr) sys.stdout.write(Colors.ENDC) elif formatter is not None: toprint = formatter(printstr, width) sys.stdout.write(toprint) else: sys.stdout.write(trunc_printstr) sys.stdout.write(' '*(width-len(printstr))) sys.stdout.write(' '*padding) if is_heading: sys.stdout.write(Colors.ENDC) is_heading = False sys.stdout.write('\n') sys.stdout.flush() def print_requests(requests): """ Takes in a list of requests and prints a table with data on each of the requests. It's the same table that's used by ``ls``. """ rows = [] for req in requests: rows.append(get_req_data_row(req)) print_request_rows(rows) def print_request_rows(request_rows): """ Takes in a list of request rows generated from :func:`pappyproxy.console.get_req_data_row` and prints a table with data on each of the requests. Used instead of :func:`pappyproxy.console.print_requests` if you can't count on storing all the requests in memory at once. """ # Print a table with info on all the requests in the list cols = [ {'name':'ID'}, {'name':'Verb'}, {'name': 'Host'}, {'name':'Path', 'width':40}, {'name':'S-Code', 'width':16}, {'name':'Req Len'}, {'name':'Rsp Len'}, {'name':'Time'}, {'name':'Mngl'}, ] print_rows = [] for row in request_rows: (reqid, verb, host, path, scode, qlen, slen, time, mngl) = row verb = {'data':verb, 'color':verb_color(verb)} scode = {'data':scode, 'color':scode_color(scode)} host = {'data':host, 'color':host_color(host)} path = {'data':path, 'formatter':path_formatter} print_rows.append((reqid, verb, host, path, scode, qlen, slen, time, mngl)) print_table(cols, print_rows) def get_req_data_row(request): """ Get the row data for a request to be printed. """ rid = request.reqid method = request.verb if 'host' in request.headers: host = request.headers['host'] else: host = '??' path = request.full_path reqlen = len(request.body) rsplen = 'N/A' mangle_str = '--' if request.unmangled: mangle_str = 'q' if request.response: response_code = str(request.response.response_code) + \ ' ' + request.response.response_text rsplen = len(request.response.body) if request.response.unmangled: if mangle_str == '--': mangle_str = 's' else: mangle_str += '/s' else: response_code = '' time_str = '--' if request.time_start and request.time_end: time_delt = request.time_end - request.time_start time_str = "%.2f" % time_delt.total_seconds() return [rid, method, host, path, response_code, reqlen, rsplen, time_str, mangle_str] def confirm(message, default='n'): """ A helper function to get confirmation from the user. It prints ``message`` then asks the user to answer yes or no. Returns True if the user answers yes, otherwise returns False. """ if 'n' in default.lower(): default = False else: default = True print message if default: answer = raw_input('(Y/n) ') else: answer = raw_input('(y/N) ') if not answer: return default if answer[0].lower() == 'y': return True else: return False ########## ## Classes class ProxyCmd(cmd2.Cmd): """ An object representing the console interface. Provides methods to add commands and aliases to the console. """ def __init__(self, *args, **kwargs): self.prompt = 'pappy' + Colors.YELLOW + '> ' + Colors.ENDC self.debug = True self._cmds = {} self._aliases = {} atexit.register(self.save_histfile) readline.set_history_length(config.HISTSIZE) if os.path.exists('cmdhistory'): if config.HISTSIZE != 0: readline.read_history_file('cmdhistory') else: os.remove('cmdhistory') cmd2.Cmd.__init__(self, *args, **kwargs) def __dir__(self): # Hack to get cmd2 to detect that we can run a command ret = set(dir(self.__class__)) ret.update(self.__dict__.keys()) ret.update(['do_'+k for k in self._cmds.keys()]) ret.update(['help_'+k for k in self._cmds.keys()]) ret.update(['complete_'+k for k, v in self._cmds.iteritems() if self._cmds[k][1]]) for k, v in self._aliases.iteritems(): ret.add('do_' + k) ret.add('help_' + k) if self._cmds[self._aliases[k]][1]: ret.add('complete_'+k) return sorted(ret) def __getattr__(self, attr): def gen_helpfunc(func): def f(): if not func.__doc__: to_print = 'No help exists for function' lines = func.__doc__.splitlines() if len(lines) > 0 and lines[0] == '': lines = lines[1:] if len(lines) > 0 and lines[-1] == '': lines = lines[-1:] to_print = '\n'.join(string.lstrip(l) for l in lines) print to_print return f if attr.startswith('do_'): command = attr[3:] if command in self._cmds: return print_pappy_errors(self._cmds[command][0]) elif command in self._aliases: real_command = self._aliases[command] if real_command in self._cmds: return print_pappy_errors(self._cmds[real_command][0]) elif attr.startswith('help_'): command = attr[5:] if command in self._cmds: return gen_helpfunc(self._cmds[command][0]) elif command in self._aliases: real_command = self._aliases[command] if real_command in self._cmds: return gen_helpfunc(self._cmds[real_command][0]) elif attr.startswith('complete_'): command = attr[9:] if command in self._cmds: if self._cmds[command][1]: return self._cmds[command][1] elif command in self._aliases: real_command = self._aliases[command] if real_command in self._cmds: if self._cmds[real_command][1]: return self._cmds[real_command][1] raise AttributeError(attr) def save_histfile(self): # Write the command to the history file if config.HISTSIZE != 0: readline.set_history_length(config.HISTSIZE) readline.write_history_file('cmdhistory') def get_names(self): # Hack to get cmd to recognize do_/etc functions as functions for things # like autocomplete return dir(self) def set_cmd(self, command, func, autocomplete_func=None): """ Add a command to the console. """ self._cmds[command] = (func, autocomplete_func) def set_cmds(self, cmd_dict): """ Set multiple commands from a dictionary. Format is: {'command': (do_func, autocomplete_func)} Use autocomplete_func=None for no autocomplete function """ for command, vals in cmd_dict.iteritems(): do_func, ac_func = vals self.set_cmd(command, do_func, ac_func) def add_alias(self, command, alias): """ Add an alias for a command. ie add_alias("foo", "f") will let you run the 'foo' command with 'f' """ self._aliases[alias] = command def add_aliases(self, alias_list): """ Pass in a list of tuples to add them all as aliases. ie add_aliases([('foo', 'f'), ('foo', 'fo')]) will add 'f' and 'fo' as aliases for 'foo' """ for command, alias in alias_list: self.add_alias(command, alias) # Taken from http://stackoverflow.com/questions/16571150/how-to-capture-stdout-output-from-a-python-function-call # then modified class Capturing(): def __enter__(self): self._stdout = sys.stdout sys.stdout = self._stringio = StringIO.StringIO() return self def __exit__(self, *args): self.val = self._stringio.getvalue() sys.stdout = self._stdout