From cdd5a53767e51a6d992bf8d08df2733e7af916b8 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Tue, 7 Feb 2012 16:39:37 +1300 Subject: Refactor console. Split the console implementation out into logical components. --- libmproxy/console/__init__.py | 993 ++++++++++++++++++++++++++++++++++++++++++ libmproxy/console/common.py | 148 +++++++ libmproxy/console/connlist.py | 100 +++++ libmproxy/console/connview.py | 490 +++++++++++++++++++++ libmproxy/console/help.py | 146 +++++++ libmproxy/console/kveditor.py | 70 +++ 6 files changed, 1947 insertions(+) create mode 100644 libmproxy/console/__init__.py create mode 100644 libmproxy/console/common.py create mode 100644 libmproxy/console/connlist.py create mode 100644 libmproxy/console/connview.py create mode 100644 libmproxy/console/help.py create mode 100644 libmproxy/console/kveditor.py (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py new file mode 100644 index 00000000..b578f924 --- /dev/null +++ b/libmproxy/console/__init__.py @@ -0,0 +1,993 @@ +# Copyright (C) 2010 Aldo Cortesi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import mailcap, mimetypes, tempfile, os, subprocess, glob, time, re +import os.path, sys +import cStringIO +import urwid +from .. import controller, utils, filt, flow +import connlist, connview, help, common, kveditor + +EVENTLOG_SIZE = 500 + + +class Stop(Exception): pass + + +#begin nocover + +class EventListBox(urwid.ListBox): + def __init__(self, master): + self.master = master + urwid.ListBox.__init__(self, master.eventlist) + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "C": + self.master.clear_events() + key = None + return urwid.ListBox.keypress(self, size, key) + + + +class _PathCompleter: + def __init__(self, _testing=False): + """ + _testing: disables reloading of the lookup table to make testing possible. + """ + self.lookup, self.offset = None, None + self.final = None + self._testing = _testing + + def reset(self): + self.lookup = None + self.offset = -1 + + def complete(self, txt): + """ + Returns the next completion for txt, or None if there is no completion. + """ + path = os.path.expanduser(txt) + if not self.lookup: + if not self._testing: + # Lookup is a set of (display value, actual value) tuples. + self.lookup = [] + if os.path.isdir(path): + files = glob.glob(os.path.join(path, "*")) + prefix = txt + else: + files = glob.glob(path+"*") + prefix = os.path.dirname(txt) + prefix = prefix or "./" + for f in files: + display = os.path.join(prefix, os.path.basename(f)) + if os.path.isdir(f): + display += "/" + self.lookup.append((display, f)) + if not self.lookup: + self.final = path + return path + self.lookup.sort() + self.offset = -1 + self.lookup.append((txt, txt)) + self.offset += 1 + if self.offset >= len(self.lookup): + self.offset = 0 + ret = self.lookup[self.offset] + self.final = ret[1] + return ret[0] + + +class PathEdit(urwid.Edit, _PathCompleter): + def __init__(self, *args, **kwargs): + urwid.Edit.__init__(self, *args, **kwargs) + _PathCompleter.__init__(self) + + def keypress(self, size, key): + if key == "tab": + comp = self.complete(self.get_edit_text()) + self.set_edit_text(comp) + self.set_edit_pos(len(comp)) + else: + self.reset() + return urwid.Edit.keypress(self, size, key) + + +class ActionBar(common.WWrap): + def __init__(self): + self.message("") + + def selectable(self): + return True + + def path_prompt(self, prompt, text): + self.w = PathEdit(prompt, text) + + def prompt(self, prompt, text = ""): + self.w = urwid.Edit(prompt, text or "") + + def message(self, message): + self.w = urwid.Text(message) + + +class StatusBar(common.WWrap): + def __init__(self, master, helptext): + self.master, self.helptext = master, helptext + self.expire = None + self.ab = ActionBar() + self.ib = common.WWrap(urwid.Text("")) + self.w = urwid.Pile([self.ib, self.ab]) + + def get_status(self): + r = [] + + if self.master.client_playback: + r.append("[") + r.append(("statusbar_highlight", "cplayback")) + r.append(":%s to go]"%self.master.client_playback.count()) + if self.master.server_playback: + r.append("[") + r.append(("statusbar_highlight", "splayback")) + r.append(":%s to go]"%self.master.server_playback.count()) + if self.master.state.intercept_txt: + r.append("[") + r.append(("statusbar_highlight", "i")) + r.append(":%s]"%self.master.state.intercept_txt) + if self.master.state.limit_txt: + r.append("[") + r.append(("statusbar_highlight", "l")) + r.append(":%s]"%self.master.state.limit_txt) + if self.master.stickycookie_txt: + r.append("[") + r.append(("statusbar_highlight", "t")) + r.append(":%s]"%self.master.stickycookie_txt) + if self.master.stickyauth_txt: + r.append("[") + r.append(("statusbar_highlight", "u")) + r.append(":%s]"%self.master.stickyauth_txt) + + opts = [] + if self.master.anticache: + opts.append("anticache") + if self.master.anticomp: + opts.append("anticomp") + if not self.master.refresh_server_playback: + opts.append("norefresh") + if self.master.killextra: + opts.append("killextra") + + if opts: + r.append("[%s]"%(":".join(opts))) + + if self.master.script: + r.append("[script:%s]"%self.master.script.path) + + if self.master.debug: + r.append("[lt:%0.3f]"%self.master.looptime) + + return r + + def redraw(self): + if self.expire and time.time() > self.expire: + self.message("") + + t = [ + ('statusbar_text', ("[%s]"%self.master.state.flow_count()).ljust(7)), + ] + t.extend(self.get_status()) + + if self.master.server: + boundaddr = "[%s:%s]"%(self.master.server.address or "*", self.master.server.port) + else: + boundaddr = "" + + status = urwid.AttrWrap(urwid.Columns([ + urwid.Text(t), + urwid.Text( + [ + self.helptext, + " ", + ('statusbar_text', "["), + ('statusbar_key', "m"), + ('statusbar_text', (":%s]"%common.BODY_VIEWS[self.master.state.view_body_mode])), + ('statusbar_text', boundaddr), + ], + align="right" + ), + ]), "statusbar") + self.ib.set_w(status) + + def update(self, text): + self.helptext = text + self.redraw() + self.master.drawscreen() + + def selectable(self): + return True + + def get_edit_text(self): + return self.ab.w.get_edit_text() + + def path_prompt(self, prompt, text): + return self.ab.path_prompt(prompt, text) + + def prompt(self, prompt, text = ""): + self.ab.prompt(prompt, text) + + def message(self, msg, expire=None): + if expire: + self.expire = time.time() + float(expire)/1000 + else: + self.expire = None + self.ab.message(msg) + + +#end nocover + +class ConsoleState(flow.State): + def __init__(self): + flow.State.__init__(self) + self.focus = None + self.view_body_mode = common.VIEW_BODY_PRETTY + self.view_flow_mode = common.VIEW_FLOW_REQUEST + self.last_script = "" + self.last_saveload = "" + + def add_request(self, req): + f = flow.State.add_request(self, req) + if self.focus is None: + self.set_focus(0) + return f + + def add_response(self, resp): + f = flow.State.add_response(self, resp) + if self.focus is None: + self.set_focus(0) + return f + + def set_limit(self, limit): + ret = flow.State.set_limit(self, limit) + self.set_focus(self.focus) + return ret + + def get_focus(self): + if not self.view or self.focus is None: + return None, None + return self.view[self.focus], self.focus + + def set_focus(self, idx): + if self.view: + if idx >= len(self.view): + idx = len(self.view) - 1 + elif idx < 0: + idx = 0 + self.focus = idx + + def get_from_pos(self, pos): + if len(self.view) <= pos or pos < 0: + return None, None + return self.view[pos], pos + + def get_next(self, pos): + return self.get_from_pos(pos+1) + + def get_prev(self, pos): + return self.get_from_pos(pos-1) + + def delete_flow(self, f): + ret = flow.State.delete_flow(self, f) + self.set_focus(self.focus) + return ret + + + +class Options(object): + __slots__ = [ + "anticache", + "anticomp", + "client_replay", + "debug", + "eventlog", + "keepserving", + "kill", + "intercept", + "no_server", + "refresh_server_playback", + "rfile", + "script", + "rheaders", + "server_replay", + "stickycookie", + "stickyauth", + "verbosity", + "wfile", + ] + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + for i in self.__slots__: + if not hasattr(self, i): + setattr(self, i, None) + + +#begin nocover + +class BodyPile(urwid.Pile): + def __init__(self, master): + h = urwid.Text("Event log") + h = urwid.Padding(h, align="left", width=("relative", 100)) + + self.inactive_header = urwid.AttrWrap(h, "inactive_heading") + self.active_header = urwid.AttrWrap(h, "heading") + + urwid.Pile.__init__( + self, + [ + connlist.ConnectionListBox(master), + urwid.Frame(EventListBox(master), header = self.inactive_header) + ] + ) + self.master = master + self.focus = 0 + + def keypress(self, size, key): + if key == "tab": + self.focus = (self.focus + 1)%len(self.widget_list) + self.set_focus(self.focus) + if self.focus == 1: + self.widget_list[1].header = self.active_header + else: + self.widget_list[1].header = self.inactive_header + key = None + elif key == "v": + self.master.toggle_eventlog() + key = None + + # This is essentially a copypasta from urwid.Pile's keypress handler. + # So much for "closed for modification, but open for extension". + item_rows = None + if len(size)==2: + item_rows = self.get_item_rows( size, focus=True ) + i = self.widget_list.index(self.focus_item) + tsize = self.get_item_size(size,i,True,item_rows) + return self.focus_item.keypress( tsize, key ) + + +class ConsoleMaster(flow.FlowMaster): + palette = [] + footer_text_default = [ + ('statusbar_key', "?"), ":help ", + ] + footer_text_help = [ + ('statusbar_key', "q"), ":back", + ] + footer_text_connview = [ + ('statusbar_key', "tab"), ":toggle view ", + ('statusbar_key', "?"), ":help ", + ('statusbar_key', "q"), ":back ", + ] + def __init__(self, server, options): + flow.FlowMaster.__init__(self, server, ConsoleState()) + self.looptime = 0 + self.options = options + + self.conn_list_view = None + self.set_palette() + + r = self.set_intercept(options.intercept) + if r: + print >> sys.stderr, "Intercept error:", r + sys.exit(1) + + r = self.set_stickycookie(options.stickycookie) + if r: + print >> sys.stderr, "Sticky cookies error:", r + sys.exit(1) + + r = self.set_stickyauth(options.stickyauth) + if r: + print >> sys.stderr, "Sticky auth error:", r + sys.exit(1) + + self.refresh_server_playback = options.refresh_server_playback + self.anticache = options.anticache + self.anticomp = options.anticomp + self.killextra = options.kill + self.rheaders = options.rheaders + + self.eventlog = options.eventlog + self.eventlist = urwid.SimpleListWalker([]) + + if options.client_replay: + self.client_playback_path(options.client_replay) + + if options.server_replay: + self.server_playback_path(options.server_replay) + + self.debug = options.debug + + if options.script: + err = self.load_script(options.script) + if err: + print >> sys.stderr, "Script load error:", err + sys.exit(1) + + def run_script_once(self, path, f): + ret = self.get_script(path) + if ret[0]: + self.statusbar.message(ret[0]) + s = ret[1] + + if f.request: + s.run("request", f) + if f.response: + s.run("response", f) + if f.error: + s.run("error", f) + s.run("done") + self.refresh_connection(f) + self.state.last_script = path + + def set_script(self, path): + if not path: + return + ret = self.load_script(path) + if ret: + self.statusbar.message(ret) + self.state.last_script = path + + def toggle_eventlog(self): + self.eventlog = not self.eventlog + self.view_connlist() + + def _readflow(self, path): + path = os.path.expanduser(path) + try: + f = file(path, "r") + flows = list(flow.FlowReader(f).stream()) + except (IOError, flow.FlowReadError), v: + return True, v.strerror + return False, flows + + def client_playback_path(self, path): + err, ret = self._readflow(path) + if err: + self.statusbar.message(ret) + else: + self.start_client_playback(ret, False) + + def server_playback_path(self, path): + err, ret = self._readflow(path) + if err: + self.statusbar.message(ret) + else: + self.start_server_playback( + ret, + self.killextra, self.rheaders, + False + ) + + def spawn_external_viewer(self, data, contenttype): + if contenttype: + ext = mimetypes.guess_extension(contenttype) or "" + else: + ext = "" + fd, name = tempfile.mkstemp(ext, "mproxy") + os.write(fd, data) + os.close(fd) + + cmd = None + shell = False + + if contenttype: + c = mailcap.getcaps() + cmd, _ = mailcap.findmatch(c, contenttype, filename=name) + if cmd: + shell = True + if not cmd: + c = os.environ.get("PAGER") or os.environ.get("EDITOR") + cmd = [c, name] + self.ui.stop() + subprocess.call(cmd, shell=shell) + self.ui.start() + os.unlink(name) + + def set_palette(self): + BARBG = "dark blue" + self.palette = [ + ('body', 'black', 'dark cyan', 'standout'), + ('foot', 'light gray', 'default'), + ('title', 'white,bold', 'default',), + ('editline', 'white', 'default',), + + # Status bar + ('statusbar', 'light gray', BARBG), + ('statusbar_key', 'light cyan', BARBG), + ('statusbar_text', 'light gray', BARBG), + ('statusbar_highlight', 'white', BARBG), + + # Help + ('key', 'light cyan', 'default', 'underline'), + ('head', 'white,bold', 'default'), + ('text', 'light gray', 'default'), + + # List and Connections + ('method', 'dark cyan', 'default'), + ('focus', 'yellow', 'default'), + ('goodcode', 'light green', 'default'), + ('error', 'light red', 'default'), + ('header', 'dark cyan', 'default'), + ('heading', 'white,bold', 'dark blue'), + ('inactive_heading', 'white', 'dark gray'), + ('highlight', 'white,bold', 'default'), + ('inactive', 'dark gray', 'default'), + ('ack', 'light red', 'default'), + + # Hex view + ('offset', 'dark cyan', 'default'), + + # KV Editor + ('editfield', 'black', 'light cyan'), + ] + + def run(self): + self.currentflow = None + + self.ui = urwid.raw_display.Screen() + self.ui.register_palette(self.palette) + self.conn_list_view = connlist.ConnectionListView(self, self.state) + + self.view = None + self.statusbar = None + self.header = None + self.body = None + + self.prompting = False + self.onekey = False + self.view_connlist() + + if self.server: + slave = controller.Slave(self.masterq, self.server) + slave.start() + + if self.options.rfile: + ret = self.load_flows(self.options.rfile) + if ret: + self.shutdown() + print >> sys.stderr, "Could not load file:", ret + sys.exit(1) + + self.ui.run_wrapper(self.loop) + # If True, quit just pops out to connection list view. + print >> sys.stderr, "Shutting down..." + sys.stderr.flush() + self.shutdown() + + def make_view(self): + self.view = urwid.Frame( + self.body, + header = self.header, + footer = self.statusbar + ) + self.view.set_focus("body") + + def view_help(self): + self.statusbar = StatusBar(self, self.footer_text_help) + self.body = help.HelpView(self) + self.header = None + self.make_view() + + def view_kveditor(self, title, value, callback): + self.statusbar = StatusBar(self, "foo") + self.body = kveditor.KVEditor(self, title, value, callback) + self.header = None + self.make_view() + + def focus_current(self): + if self.currentflow: + try: + ids = [id(i) for i in self.state.view] + idx = ids.index(id(self.currentflow)) + self.conn_list_view.set_focus(idx) + except (IndexError, ValueError): + pass + + def view_connlist(self): + if self.ui.started: + self.ui.clear() + self.focus_current() + if self.eventlog: + self.body = BodyPile(self) + else: + self.body = connlist.ConnectionListBox(self) + self.statusbar = StatusBar(self, self.footer_text_default) + self.header = None + self.currentflow = None + self.make_view() + + def view_flow(self, flow): + self.statusbar = StatusBar(self, self.footer_text_connview) + self.body = connview.ConnectionView(self, self.state, flow) + self.header = connview.ConnectionViewHeader(self, flow) + self.currentflow = flow + self.make_view() + + def _view_nextprev_flow(self, np, flow): + try: + idx = self.state.view.index(flow) + except IndexError: + return + if np == "next": + new_flow, new_idx = self.state.get_next(idx) + else: + new_flow, new_idx = self.state.get_prev(idx) + if new_idx is None: + return + self.view_flow(new_flow) + + def view_next_flow(self, flow): + return self._view_nextprev_flow("next", flow) + + def view_prev_flow(self, flow): + return self._view_nextprev_flow("prev", flow) + + def _write_flows(self, path, flows): + self.state.last_saveload = path + if not path: + return + path = os.path.expanduser(path) + try: + f = file(path, "wb") + fw = flow.FlowWriter(f) + for i in flows: + fw.add(i) + f.close() + except IOError, v: + self.statusbar.message(v.strerror) + + def save_one_flow(self, path, flow): + return self._write_flows(path, [flow]) + + def save_flows(self, path): + return self._write_flows(path, self.state.view) + + def load_flows_callback(self, path): + if not path: + return + ret = self.load_flows(path) + return ret or "Flows loaded from %s"%path + + def load_flows(self, path): + self.state.last_saveload = path + path = os.path.expanduser(path) + try: + f = file(path, "r") + fr = flow.FlowReader(f) + except IOError, v: + return v.strerror + flow.FlowMaster.load_flows(self, fr) + f.close() + if self.conn_list_view: + self.sync_list_view() + self.focus_current() + + def path_prompt(self, prompt, text, callback, *args): + self.statusbar.path_prompt(prompt, text) + self.view.set_focus("footer") + self.prompting = (callback, args) + + def prompt(self, prompt, text, callback, *args): + self.statusbar.prompt(prompt, text) + self.view.set_focus("footer") + self.prompting = (callback, args) + + def prompt_edit(self, prompt, text, callback): + self.statusbar.prompt(prompt + ": ", text) + self.view.set_focus("footer") + self.prompting = (callback, []) + + def prompt_onekey(self, prompt, keys, callback, *args): + """ + Keys are a set of (word, key) tuples. The appropriate key in the + word is highlighted. + """ + prompt = [prompt, " ("] + mkup = [] + for i, e in enumerate(keys): + mkup.extend(common.highlight_key(e[0], e[1])) + if i < len(keys)-1: + mkup.append(",") + prompt.extend(mkup) + prompt.append(")? ") + self.onekey = "".join(i[1] for i in keys) + self.prompt(prompt, "", callback, *args) + + def prompt_done(self): + self.prompting = False + self.onekey = False + self.view.set_focus("body") + self.statusbar.message("") + + def prompt_execute(self, txt=None): + if not txt: + txt = self.statusbar.get_edit_text() + p, args = self.prompting + self.prompt_done() + msg = p(txt, *args) + if msg: + self.statusbar.message(msg, 1000) + + def prompt_cancel(self): + self.prompt_done() + + def accept_all(self): + self.state.accept_all() + + def set_limit(self, txt): + return self.state.set_limit(txt) + + def set_intercept(self, txt): + return self.state.set_intercept(txt) + + def changeview(self, v): + if v == "r": + self.state.view_body_mode = common.VIEW_BODY_RAW + elif v == "h": + self.state.view_body_mode = common.VIEW_BODY_HEX + elif v == "p": + self.state.view_body_mode = common.VIEW_BODY_PRETTY + self.refresh_connection(self.currentflow) + + def drawscreen(self): + size = self.ui.get_cols_rows() + canvas = self.view.render(size, focus=1) + self.ui.draw_screen(size, canvas) + return size + + def pop_view(self): + if self.currentflow: + self.view_flow(self.currentflow) + else: + self.view_connlist() + + def loop(self): + changed = True + try: + while not controller.should_exit: + startloop = time.time() + if changed: + self.statusbar.redraw() + size = self.drawscreen() + changed = self.tick(self.masterq) + self.ui.set_input_timeouts(max_wait=0.1) + keys = self.ui.get_input() + if keys: + changed = True + for k in keys: + if self.prompting: + if k == "esc": + self.prompt_cancel() + elif self.onekey: + if k == "enter": + self.prompt_cancel() + elif k in self.onekey: + self.prompt_execute(k) + elif k == "enter": + self.prompt_execute() + else: + self.view.keypress(size, k) + else: + k = self.view.keypress(size, k) + if k: + self.statusbar.message("") + if k == "?": + self.view_help() + elif k == "c": + if not self.client_playback: + self.path_prompt( + "Client replay: ", + self.state.last_saveload, + self.client_playback_path + ) + else: + self.prompt_onekey( + "Stop current client replay?", + ( + ("yes", "y"), + ("no", "n"), + ), + self.stop_client_playback_prompt, + ) + elif k == "l": + self.prompt("Limit: ", self.state.limit_txt, self.set_limit) + self.sync_list_view() + elif k == "i": + self.prompt( + "Intercept filter: ", + self.state.intercept_txt, + self.set_intercept + ) + self.sync_list_view() + elif k == "m": + self.prompt_onekey( + "View", + ( + ("raw", "r"), + ("pretty", "p"), + ("hex", "h"), + ), + self.changeview + ) + elif k == "Q": + raise Stop + elif k == "q": + self.prompt_onekey( + "Quit", + ( + ("yes", "y"), + ("no", "n"), + ), + self.quit, + ) + elif k == "w": + self.path_prompt( + "Save flows: ", + self.state.last_saveload, + self.save_flows + ) + elif k == "s": + if self.script: + self.load_script(None) + else: + self.path_prompt( + "Set script: ", + self.state.last_script, + self.set_script + ) + elif k == "S": + if not self.server_playback: + self.path_prompt( + "Server replay: ", + self.state.last_saveload, + self.server_playback_path + ) + else: + self.prompt_onekey( + "Stop current server replay?", + ( + ("yes", "y"), + ("no", "n"), + ), + self.stop_server_playback_prompt, + ) + elif k == "L": + self.path_prompt( + "Load flows: ", + self.state.last_saveload, + self.load_flows_callback + ) + elif k == "o": + self.prompt_onekey( + "Options", + ( + ("anticache", "a"), + ("anticomp", "c"), + ("killextra", "k"), + ("norefresh", "n"), + ), + self._change_options + ) + elif k == "t": + self.prompt( + "Sticky cookie filter: ", + self.stickycookie_txt, + self.set_stickycookie + ) + elif k == "u": + self.prompt( + "Sticky auth filter: ", + self.stickyauth_txt, + self.set_stickyauth + ) + self.looptime = time.time() - startloop + except (Stop, KeyboardInterrupt): + pass + + def stop_client_playback_prompt(self, a): + if a != "n": + self.stop_client_playback() + + def stop_server_playback_prompt(self, a): + if a != "n": + self.stop_server_playback() + + def quit(self, a): + if a != "n": + raise Stop + + def _change_options(self, a): + if a == "a": + self.anticache = not self.anticache + if a == "c": + self.anticomp = not self.anticomp + elif a == "k": + self.killextra = not self.killextra + elif a == "n": + self.refresh_server_playback = not self.refresh_server_playback + + def shutdown(self): + self.state.killall(self) + controller.Master.shutdown(self) + + def sync_list_view(self): + self.conn_list_view._modified() + + def clear_connections(self): + self.state.clear() + self.sync_list_view() + + def delete_connection(self, f): + self.state.delete_flow(f) + self.sync_list_view() + + def refresh_connection(self, c): + if hasattr(self.header, "refresh_connection"): + self.header.refresh_connection(c) + if hasattr(self.body, "refresh_connection"): + self.body.refresh_connection(c) + if hasattr(self.statusbar, "refresh_connection"): + self.statusbar.refresh_connection(c) + + def process_flow(self, f, r): + if self.state.intercept and f.match(self.state.intercept) and not f.request.is_replay(): + f.intercept() + else: + r._ack() + self.sync_list_view() + self.refresh_connection(f) + + def clear_events(self): + self.eventlist[:] = [] + + def add_event(self, e, level="info"): + if level == "info": + e = urwid.Text(e) + elif level == "error": + e = urwid.Text(("error", e)) + + self.eventlist.append(e) + if len(self.eventlist) > EVENTLOG_SIZE: + self.eventlist.pop(0) + self.eventlist.set_focus(len(self.eventlist)) + + # Handlers + def handle_error(self, r): + f = flow.FlowMaster.handle_error(self, r) + if f: + self.process_flow(f, r) + return f + + def handle_request(self, r): + f = flow.FlowMaster.handle_request(self, r) + if f: + self.process_flow(f, r) + return f + + def handle_response(self, r): + f = flow.FlowMaster.handle_response(self, r) + if f: + self.process_flow(f, r) + return f + diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py new file mode 100644 index 00000000..6fbb5e19 --- /dev/null +++ b/libmproxy/console/common.py @@ -0,0 +1,148 @@ +import urwid +from .. import utils + + +VIEW_BODY_RAW = 0 +VIEW_BODY_HEX = 1 +VIEW_BODY_PRETTY = 2 + +BODY_VIEWS = { + VIEW_BODY_RAW: "raw", + VIEW_BODY_HEX: "hex", + VIEW_BODY_PRETTY: "pretty" +} + +VIEW_FLOW_REQUEST = 0 +VIEW_FLOW_RESPONSE = 1 + + +def highlight_key(s, k): + l = [] + parts = s.split(k, 1) + if parts[0]: + l.append(("text", parts[0])) + l.append(("key", k)) + if parts[1]: + l.append(("text", parts[1])) + return l + + +def format_keyvals(lst, key="key", val="text", space=5, indent=0): + """ + Format a list of (key, value) tuples. + + If key is None, it's treated specially: + - We assume a sub-value, and add an extra indent. + - The value is treated as a pre-formatted list of directives. + """ + ret = [] + if lst: + pad = max(len(i[0]) for i in lst if i and i[0]) + space + for i in lst: + if i is None: + ret.extend("\n") + elif i[0] is None: + ret.append(" "*(pad + indent*2)) + ret.extend(i[1]) + ret.append("\n") + else: + ret.extend( + [ + " "*indent, + (key, i[0]), + " "*(pad-len(i[0])), + (val, i[1]), + "\n" + ] + ) + return ret + + +def shortcuts(k): + if k == " ": + k = "page down" + elif k == "j": + k = "down" + elif k == "k": + k = "up" + return k + + +def format_flow(f, focus, extended=False, padding=2): + txt = [] + if extended: + txt.append(("highlight", utils.format_timestamp(f.request.timestamp))) + txt.append(" ") + if f.request.is_replay(): + txt.append(("method", "[replay]")) + txt.extend([ + ("ack", "!") if f.intercepting and not f.request.acked else " ", + ("method", f.request.method), + " ", + ( + "text" if (f.response or f.error) else "title", + f.request.get_url(), + ), + ]) + if f.response or f.error or f.request.is_replay(): + tsr = f.response or f.error + if extended and tsr: + ts = ("highlight", utils.format_timestamp(tsr.timestamp) + " ") + else: + ts = " " + + txt.append("\n") + txt.append(("text", ts)) + txt.append(" "*(padding+2)) + + if f.response: + txt.append( + ("ack", "!") if f.intercepting and not f.response.acked else " " + ) + txt.append("<- ") + if f.response.is_replay(): + txt.append(("method", "[replay] ")) + if f.response.code in [200, 304]: + txt.append(("goodcode", str(f.response.code))) + else: + txt.append(("error", str(f.response.code))) + t = f.response.headers["content-type"] + if t: + t = t[0].split(";")[0] + txt.append(("text", " %s"%t)) + if f.response.content: + txt.append(", %s"%utils.pretty_size(len(f.response.content))) + elif f.error: + txt.append( + ("error", f.error.msg) + ) + + if focus: + txt.insert(0, ("focus", ">>" + " "*(padding-2))) + else: + txt.insert(0, " "*padding) + return txt + + + + +def int_version(v): + SIG = 3 + v = urwid.__version__.split("-")[0].split(".") + x = 0 + for i in range(min(SIG, len(v))): + x += int(v[i]) * 10**(SIG-i) + return x + + +# We have to do this to be portable over 0.9.8 and 0.9.9 If compatibility +# becomes a pain to maintain, we'll just mandate 0.9.9 or newer. +class WWrap(urwid.WidgetWrap): + if int_version(urwid.__version__) >= 990: + def set_w(self, x): + self._w = x + def get_w(self): + return self._w + w = property(get_w, set_w) + + diff --git a/libmproxy/console/connlist.py b/libmproxy/console/connlist.py new file mode 100644 index 00000000..3fdfb0ed --- /dev/null +++ b/libmproxy/console/connlist.py @@ -0,0 +1,100 @@ +import urwid +import common + +class ConnectionItem(common.WWrap): + def __init__(self, master, state, flow, focus): + self.master, self.state, self.flow = master, state, flow + self.focus = focus + w = self.get_text() + common.WWrap.__init__(self, w) + + def get_text(self): + return urwid.Text(common.format_flow(self.flow, self.focus)) + + def selectable(self): + return True + + def keypress(self, (maxcol,), key): + key = common.shortcuts(key) + if key == "a": + self.flow.accept_intercept() + self.master.sync_list_view() + elif key == "d": + self.flow.kill(self.master) + self.state.delete_flow(self.flow) + self.master.sync_list_view() + elif key == "r": + r = self.master.replay_request(self.flow) + if r: + self.master.statusbar.message(r) + self.master.sync_list_view() + elif key == "R": + self.state.revert(self.flow) + self.master.sync_list_view() + elif key == "W": + self.master.path_prompt( + "Save this flow: ", + self.state.last_saveload, + self.master.save_one_flow, + self.flow + ) + elif key == "X": + self.flow.kill(self.master) + elif key == "v": + self.master.toggle_eventlog() + elif key == "enter": + if self.flow.request: + self.master.view_flow(self.flow) + elif key == "|": + self.master.path_prompt( + "Send flow to script: ", self.state.last_script, + self.master.run_script_once, self.flow + ) + return key + + +class ConnectionListView(urwid.ListWalker): + def __init__(self, master, state): + self.master, self.state = master, state + if self.state.flow_count(): + self.set_focus(0) + + def get_focus(self): + f, i = self.state.get_focus() + f = ConnectionItem(self.master, self.state, f, True) if f else None + return f, i + + def set_focus(self, focus): + ret = self.state.set_focus(focus) + self._modified() + return ret + + def get_next(self, pos): + f, i = self.state.get_next(pos) + f = ConnectionItem(self.master, self.state, f, False) if f else None + return f, i + + def get_prev(self, pos): + f, i = self.state.get_prev(pos) + f = ConnectionItem(self.master, self.state, f, False) if f else None + return f, i + + +class ConnectionListBox(urwid.ListBox): + def __init__(self, master): + self.master = master + urwid.ListBox.__init__(self, master.conn_list_view) + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "A": + self.master.accept_all() + self.master.sync_list_view() + key = None + elif key == "C": + self.master.clear_connections() + key = None + elif key == "v": + self.master.toggle_eventlog() + key = None + return urwid.ListBox.keypress(self, size, key) diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py new file mode 100644 index 00000000..1e9d0c60 --- /dev/null +++ b/libmproxy/console/connview.py @@ -0,0 +1,490 @@ +import urwid +import common +from .. import utils, encoding + +VIEW_CUTOFF = 1024*100 + +class ConnectionViewHeader(common.WWrap): + def __init__(self, master, f): + self.master, self.flow = master, f + self.w = urwid.Text(common.format_flow(f, False, extended=True, padding=0)) + + def refresh_connection(self, f): + if f == self.flow: + self.w = urwid.Text(common.format_flow(f, False, extended=True, padding=0)) + + +class CallbackCache: + @utils.LRUCache(20) + def callback(self, obj, method, *args, **kwargs): + return getattr(obj, method)(*args, **kwargs) +cache = CallbackCache() + + +class ConnectionView(common.WWrap): + REQ = 0 + RESP = 1 + methods = [ + ("get", "g"), + ("post", "p"), + ("put", "u"), + ("head", "h"), + ("trace", "t"), + ("delete", "d"), + ("options", "o"), + ] + def __init__(self, master, state, flow): + self.master, self.state, self.flow = master, state, flow + if self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE and flow.response: + self.view_response() + else: + self.view_request() + + def _trailer(self, clen, txt): + rem = clen - VIEW_CUTOFF + if rem > 0: + txt.append(urwid.Text("")) + txt.append( + urwid.Text( + [ + ("highlight", "... %s of data not shown"%utils.pretty_size(rem)) + ] + ) + ) + + def _view_conn_raw(self, content): + txt = [] + for i in utils.cleanBin(content[:VIEW_CUTOFF]).splitlines(): + txt.append( + urwid.Text(("text", i)) + ) + self._trailer(len(content), txt) + return txt + + def _view_conn_binary(self, content): + txt = [] + for offset, hexa, s in utils.hexdump(content[:VIEW_CUTOFF]): + txt.append(urwid.Text([ + ("offset", offset), + " ", + ("text", hexa), + " ", + ("text", s), + ])) + self._trailer(len(content), txt) + return txt + + def _view_conn_xmlish(self, content): + txt = [] + for i in utils.pretty_xmlish(content[:VIEW_CUTOFF]): + txt.append( + urwid.Text(("text", i)), + ) + self._trailer(len(content), txt) + return txt + + def _view_conn_json(self, lines): + txt = [] + sofar = 0 + for i in lines: + sofar += len(i) + txt.append( + urwid.Text(("text", i)), + ) + if sofar > VIEW_CUTOFF: + break + self._trailer(sum(len(i) for i in lines), txt) + return txt + + def _view_conn_formdata(self, content, boundary): + rx = re.compile(r'\bname="([^"]+)"') + keys = [] + vals = [] + + for i in content.split("--" + boundary): + parts = i.splitlines() + if len(parts) > 1 and parts[0][0:2] != "--": + match = rx.search(parts[1]) + if match: + keys.append(match.group(1) + ":") + vals.append(utils.cleanBin( + "\n".join(parts[3+parts[2:].index(""):]) + )) + kv = common.format_keyvals( + zip(keys, vals), + key = "header", + val = "text" + ) + return [ + urwid.Text(("highlight", "Form data:\n")), + urwid.Text(kv) + ] + + def _view_conn_urlencoded(self, lines): + kv = common.format_keyvals( + [(k+":", v) for (k, v) in lines], + key = "header", + val = "text" + ) + return [ + urwid.Text(("highlight", "URLencoded data:\n")), + urwid.Text(kv) + ] + + def _find_pretty_view(self, content, hdrItems): + ctype = None + for i in hdrItems: + if i[0].lower() == "content-type": + ctype = i[1] + break + if ctype and "x-www-form-urlencoded" in ctype: + data = utils.urldecode(content) + if data: + return self._view_conn_urlencoded(data) + if utils.isXML(content): + return self._view_conn_xmlish(content) + elif ctype and "application/json" in ctype: + lines = utils.pretty_json(content) + if lines: + return self._view_conn_json(lines) + elif ctype and "multipart/form-data" in ctype: + boundary = ctype.split('boundary=') + if len(boundary) > 1: + return self._view_conn_formdata(content, boundary[1].split(';')[0]) + return self._view_conn_raw(content) + + def _cached_conn_text(self, e, content, hdrItems, viewmode): + hdr = [] + hdr.extend( + common.format_keyvals( + [(h+":", v) for (h, v) in hdrItems], + key = "header", + val = "text" + ) + ) + hdr.append("\n") + + txt = [urwid.Text(hdr)] + if content: + if viewmode == common.VIEW_BODY_HEX: + txt.extend(self._view_conn_binary(content)) + elif viewmode == common.VIEW_BODY_PRETTY: + if e: + decoded = encoding.decode(e, content) + if decoded: + content = decoded + if e and e != "identity": + txt.append( + urwid.Text(("highlight", "Decoded %s data:\n"%e)) + ) + txt.extend(self._find_pretty_view(content, hdrItems)) + else: + txt.extend(self._view_conn_raw(content)) + return urwid.ListBox(txt) + + + + + def _tab(self, content, active): + if active: + attr = "heading" + else: + attr = "inactive" + p = urwid.Text(content) + p = urwid.Padding(p, align="left", width=("relative", 100)) + p = urwid.AttrWrap(p, attr) + return p + + def wrap_body(self, active, body): + parts = [] + + if self.flow.intercepting and not self.flow.request.acked: + qt = "Request (intercepted)" + else: + qt = "Request" + if active == common.VIEW_FLOW_REQUEST: + parts.append(self._tab(qt, True)) + else: + parts.append(self._tab(qt, False)) + + if self.flow.intercepting and not self.flow.response.acked: + st = "Response (intercepted)" + else: + st = "Response" + if active == common.VIEW_FLOW_RESPONSE: + parts.append(self._tab(st, True)) + else: + parts.append(self._tab(st, False)) + + h = urwid.Columns(parts, dividechars=1) + f = urwid.Frame( + body, + header=h + ) + return f + + def _conn_text(self, conn, viewmode): + e = conn.headers["content-encoding"] + e = e[0] if e else None + return cache.callback( + self, "_cached_conn_text", + e, + conn.content, + tuple(tuple(i) for i in conn.headers.lst), + viewmode + ) + + def view_request(self): + self.state.view_flow_mode = common.VIEW_FLOW_REQUEST + self.master.statusbar.update("Calculating view...") + body = self._conn_text( + self.flow.request, + self.state.view_body_mode + ) + self.w = self.wrap_body(common.VIEW_FLOW_REQUEST, body) + self.master.statusbar.update("") + + def view_response(self): + self.state.view_flow_mode = common.VIEW_FLOW_RESPONSE + self.master.statusbar.update("Calculating view...") + if self.flow.response: + body = self._conn_text( + self.flow.response, + self.state.view_body_mode + ) + else: + body = urwid.ListBox( + [ + urwid.Text(""), + urwid.Text( + [ + ("highlight", "No response. Press "), + ("key", "e"), + ("highlight", " and edit any aspect to add one."), + ] + ) + ] + ) + self.w = self.wrap_body(common.VIEW_FLOW_RESPONSE, body) + self.master.statusbar.update("") + + def refresh_connection(self, c=None): + if c == self.flow: + if self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE and self.flow.response: + self.view_response() + else: + self.view_request() + + def _spawn_editor(self, data): + fd, name = tempfile.mkstemp('', "mproxy") + os.write(fd, data) + os.close(fd) + c = os.environ.get("EDITOR") + #If no EDITOR is set, assume 'vi' + if not c: + c = "vi" + cmd = [c, name] + self.master.ui.stop() + try: + subprocess.call(cmd) + except: + self.master.statusbar.message("Can't start editor: %s" % c) + self.master.ui.start() + os.unlink(name) + return data + self.master.ui.start() + data = open(name).read() + os.unlink(name) + return data + + def edit_method(self, m): + for i in self.methods: + if i[1] == m: + self.flow.request.method = i[0].upper() + self.master.refresh_connection(self.flow) + + def save_body(self, path): + if not path: + return + self.state.last_saveload = path + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + c = self.flow.request + else: + c = self.flow.response + path = os.path.expanduser(path) + try: + f = file(path, "wb") + f.write(str(c.content)) + f.close() + except IOError, v: + self.master.statusbar.message(v.strerror) + + def set_url(self, url): + request = self.flow.request + if not request.set_url(str(url)): + return "Invalid URL." + self.master.refresh_connection(self.flow) + + def set_resp_code(self, code): + response = self.flow.response + try: + response.code = int(code) + except ValueError: + return None + import BaseHTTPServer + if BaseHTTPServer.BaseHTTPRequestHandler.responses.has_key(int(code)): + response.msg = BaseHTTPServer.BaseHTTPRequestHandler.responses[int(code)][0] + self.master.refresh_connection(self.flow) + + def set_resp_msg(self, msg): + response = self.flow.response + response.msg = msg + self.master.refresh_connection(self.flow) + + def edit(self, part): + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + conn = self.flow.request + else: + if not self.flow.response: + self.flow.response = flow.Response(self.flow.request, 200, "OK", flow.Headers(), "") + conn = self.flow.response + + self.flow.backup() + if part == "b": + c = self._spawn_editor(conn.content or "") + conn.content = c.rstrip("\n") + elif part == "h": + self.master.view_kveditor("Editing headers", conn.headers.lst, None) + #headertext = self._spawn_editor(repr(conn.headers)) + #headers = flow.Headers() + #fp = cStringIO.StringIO(headertext) + #headers.read(fp) + #conn.headers = headers + elif part == "u" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + self.master.prompt_edit("URL", conn.get_url(), self.set_url) + elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + self.master.prompt_onekey("Method", self.methods, self.edit_method) + elif part == "c" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: + self.master.prompt_edit("Code", str(conn.code), self.set_resp_code) + elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: + self.master.prompt_edit("Message", conn.msg, self.set_resp_msg) + self.master.refresh_connection(self.flow) + + def keypress(self, size, key): + if key == " ": + self.master.view_next_flow(self.flow) + return key + + key = common.shortcuts(key) + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + conn = self.flow.request + else: + conn = self.flow.response + + if key == "q": + self.master.view_connlist() + key = None + elif key == "tab": + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + self.view_response() + else: + self.view_request() + elif key in ("up", "down", "page up", "page down"): + # Why doesn't this just work?? + self.w.body.keypress(size, key) + elif key == "a": + self.flow.accept_intercept() + self.master.view_flow(self.flow) + elif key == "A": + self.master.accept_all() + self.master.view_flow(self.flow) + elif key == "e": + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + self.master.prompt_onekey( + "Edit request", + ( + ("header", "h"), + ("body", "b"), + ("url", "u"), + ("method", "m"), + ), + self.edit + ) + else: + self.master.prompt_onekey( + "Edit response", + ( + ("code", "c"), + ("message", "m"), + ("header", "h"), + ("body", "b"), + ), + self.edit + ) + key = None + elif key == "p": + self.master.view_prev_flow(self.flow) + elif key == "r": + r = self.master.replay_request(self.flow) + if r: + self.master.statusbar.message(r) + self.master.refresh_connection(self.flow) + elif key == "R": + self.state.revert(self.flow) + self.master.refresh_connection(self.flow) + elif key == "W": + self.master.path_prompt( + "Save this flow: ", + self.state.last_saveload, + self.master.save_one_flow, + self.flow + ) + elif key == "v": + if conn and conn.content: + t = conn.headers["content-type"] or [None] + t = t[0] + self.master.spawn_external_viewer(conn.content, t) + elif key == "b": + if conn: + if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: + self.master.path_prompt( + "Save request body: ", + self.state.last_saveload, + self.save_body + ) + else: + self.master.path_prompt( + "Save response body: ", + self.state.last_saveload, + self.save_body + ) + elif key == "|": + self.master.path_prompt( + "Send flow to script: ", self.state.last_script, + self.master.run_script_once, self.flow + ) + elif key == "z": + if conn: + e = conn.headers["content-encoding"] or ["identity"] + if e[0] != "identity": + conn.decode() + else: + self.master.prompt_onekey( + "Select encoding: ", + ( + ("gzip", "z"), + ("deflate", "d"), + ), + self.encode_callback, + conn + ) + self.master.refresh_connection(self.flow) + return key + + def encode_callback(self, key, conn): + encoding_map = { + "z": "gzip", + "d": "deflate", + } + conn.encode(encoding_map[key]) + self.master.refresh_connection(self.flow) diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py new file mode 100644 index 00000000..574dd7ca --- /dev/null +++ b/libmproxy/console/help.py @@ -0,0 +1,146 @@ +import urwid +import common +from .. import filt + +class HelpView(urwid.ListBox): + def __init__(self, master): + self.master = master + urwid.ListBox.__init__( + self, + self.helptext() + ) + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "q": + self.master.pop_view() + return None + return urwid.ListBox.keypress(self, size, key) + + def helptext(self): + text = [] + text.append(("head", "Global keys:\n")) + keys = [ + ("A", "accept all intercepted connections"), + ("a", "accept this intercepted connection"), + ("c", "client replay"), + ("i", "set interception pattern"), + ("j, k", "up, down"), + ("l", "set limit filter pattern"), + ("L", "load saved flows"), + + ("m", "change body display mode"), + (None, + common.highlight_key("raw", "r") + + [("text", ": raw data")] + ), + (None, + common.highlight_key("pretty", "p") + + [("text", ": pretty-print XML, HTML and JSON")] + ), + (None, + common.highlight_key("hex", "h") + + [("text", ": hex dump")] + ), + + ("o", "toggle options:"), + (None, + common.highlight_key("anticache", "a") + + [("text", ": prevent cached responses")] + ), + (None, + common.highlight_key("anticomp", "c") + + [("text", ": prevent compressed responses")] + ), + (None, + common.highlight_key("killextra", "k") + + [("text", ": kill requests not part of server replay")] + ), + (None, + common.highlight_key("norefresh", "n") + + [("text", ": disable server replay response refresh")] + ), + + ("q", "quit / return to connection list"), + ("Q", "quit without confirm prompt"), + ("r", "replay request"), + ("R", "revert changes to request"), + ("s", "set/unset script"), + ("S", "server replay"), + ("t", "set sticky cookie expression"), + ("u", "set sticky auth expression"), + ("w", "save all flows matching current limit"), + ("W", "save this flow"), + ("|", "run script on this flow"), + ("space", "page down"), + ("pg up/down", "page up/down"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + + text.append(("head", "\n\nConnection list keys:\n")) + keys = [ + ("C", "clear connection list or eventlog"), + ("d", "delete connection from view"), + ("v", "toggle eventlog"), + ("X", "kill and delete connection, even if it's mid-intercept"), + ("tab", "tab between eventlog and connection list"), + ("enter", "view connection"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + + text.append(("head", "\n\nConnection view keys:\n")) + keys = [ + ("b", "save request/response body"), + ("e", "edit request/response"), + ("p", "previous flow"), + ("v", "view body in external viewer"), + ("z", "encode/decode a request/response"), + ("tab", "toggle request/response view"), + ("space", "next flow"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + + text.append(("head", "\n\nFilter expressions:\n")) + f = [] + for i in filt.filt_unary: + f.append( + ("~%s"%i.code, i.help) + ) + for i in filt.filt_rex: + f.append( + ("~%s regex"%i.code, i.help) + ) + for i in filt.filt_int: + f.append( + ("~%s int"%i.code, i.help) + ) + f.sort() + f.extend( + [ + ("!", "unary not"), + ("&", "and"), + ("|", "or"), + ("(...)", "grouping"), + ] + ) + text.extend(common.format_keyvals(f, key="key", val="text", indent=4)) + + text.extend( + [ + "\n", + ("text", " Regexes are Python-style.\n"), + ("text", " Regexes can be specified as quoted strings.\n"), + ("text", " Header matching (~h, ~hq, ~hs) is against a string of the form \"name: value\".\n"), + ("text", " Expressions with no operators are regex matches against URL.\n"), + ("text", " Default binary operator is &.\n"), + ("head", "\n Examples:\n"), + ] + ) + examples = [ + ("google\.com", "Url containing \"google.com"), + ("~q ~b test", "Requests where body contains \"test\""), + ("!(~q & ~t \"text/html\")", "Anything but requests with a text/html content type."), + ] + text.extend(common.format_keyvals(examples, key="key", val="text", indent=4)) + return [urwid.Text(text)] + diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py new file mode 100644 index 00000000..7f2d965c --- /dev/null +++ b/libmproxy/console/kveditor.py @@ -0,0 +1,70 @@ +import urwid +import common + +class SText(common.WWrap): + def __init__(self, txt): + w = urwid.Text(txt, wrap="any") + w = urwid.AttrWrap(w, "editfield") + common.WWrap.__init__(self, w) + + def keypress(self, size, key): + raise ValueError, key + time.sleep(0.5) + return key + + def selectable(self): + return True + + +class KVEditor(common.WWrap): + def __init__(self, master, title, value, callback): + self.master, self.title, self.value, self.callback = master, title, value, callback + p = urwid.Text(title) + p = urwid.Padding(p, align="left", width=("relative", 100)) + p = urwid.AttrWrap(p, "heading") + maxk = max(len(v[0]) for v in value) + parts = [] + for k, v in value: + parts.append( + urwid.Columns( + [ + ( + "fixed", + maxk + 2, + SText(k) + ), + SText(v) + ], + dividechars = 2 + ) + ) + parts.append(urwid.Text(" ")) + self.lb = urwid.ListBox(parts) + self.w = urwid.Frame(self.lb, header = p) + self.master.statusbar.update("") + + def keypress(self, size, key): + if key == "q": + self.master.pop_view() + return None + if key in ("tab", "enter"): + cw = self.lb.get_focus()[0] + col = cw.get_focus_column() + if col == 0: + cw.set_focus_column(1) + else: + self.lb._keypress_down(size) + cw = self.lb.get_focus()[0] + cw.set_focus_column(0) + return None + elif key == "ctrl e": + # Editor + pass + elif key == "ctrl r": + # Revert + pass + elif key == "esc": + self.master.view_connlist() + return + return self.w.keypress(size, key) + -- cgit v1.2.3 From 7e6196511f5dc761d5361ac89ff4e6641881d05c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 14:07:17 +1300 Subject: Editable fields for KVEditor. --- libmproxy/console/__init__.py | 74 +------------------ libmproxy/console/connlist.py | 55 ++++++++++++++ libmproxy/console/connview.py | 23 +++++- libmproxy/console/kveditor.py | 165 +++++++++++++++++++++++++++++++----------- 4 files changed, 198 insertions(+), 119 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index b578f924..6fc76160 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -28,19 +28,6 @@ class Stop(Exception): pass #begin nocover -class EventListBox(urwid.ListBox): - def __init__(self, master): - self.master = master - urwid.ListBox.__init__(self, master.eventlist) - - def keypress(self, size, key): - key = common.shortcuts(key) - if key == "C": - self.master.clear_events() - key = None - return urwid.ListBox.keypress(self, size, key) - - class _PathCompleter: def __init__(self, _testing=False): @@ -324,46 +311,6 @@ class Options(object): #begin nocover -class BodyPile(urwid.Pile): - def __init__(self, master): - h = urwid.Text("Event log") - h = urwid.Padding(h, align="left", width=("relative", 100)) - - self.inactive_header = urwid.AttrWrap(h, "inactive_heading") - self.active_header = urwid.AttrWrap(h, "heading") - - urwid.Pile.__init__( - self, - [ - connlist.ConnectionListBox(master), - urwid.Frame(EventListBox(master), header = self.inactive_header) - ] - ) - self.master = master - self.focus = 0 - - def keypress(self, size, key): - if key == "tab": - self.focus = (self.focus + 1)%len(self.widget_list) - self.set_focus(self.focus) - if self.focus == 1: - self.widget_list[1].header = self.active_header - else: - self.widget_list[1].header = self.inactive_header - key = None - elif key == "v": - self.master.toggle_eventlog() - key = None - - # This is essentially a copypasta from urwid.Pile's keypress handler. - # So much for "closed for modification, but open for extension". - item_rows = None - if len(size)==2: - item_rows = self.get_item_rows( size, focus=True ) - i = self.widget_list.index(self.focus_item) - tsize = self.get_item_size(size,i,True,item_rows) - return self.focus_item.keypress( tsize, key ) - class ConsoleMaster(flow.FlowMaster): palette = [] @@ -609,7 +556,7 @@ class ConsoleMaster(flow.FlowMaster): self.ui.clear() self.focus_current() if self.eventlog: - self.body = BodyPile(self) + self.body = connlist.BodyPile(self) else: self.body = connlist.ConnectionListBox(self) self.statusbar = StatusBar(self, self.footer_text_default) @@ -624,25 +571,6 @@ class ConsoleMaster(flow.FlowMaster): self.currentflow = flow self.make_view() - def _view_nextprev_flow(self, np, flow): - try: - idx = self.state.view.index(flow) - except IndexError: - return - if np == "next": - new_flow, new_idx = self.state.get_next(idx) - else: - new_flow, new_idx = self.state.get_prev(idx) - if new_idx is None: - return - self.view_flow(new_flow) - - def view_next_flow(self, flow): - return self._view_nextprev_flow("next", flow) - - def view_prev_flow(self, flow): - return self._view_nextprev_flow("prev", flow) - def _write_flows(self, path, flows): self.state.last_saveload = path if not path: diff --git a/libmproxy/console/connlist.py b/libmproxy/console/connlist.py index 3fdfb0ed..319963e8 100644 --- a/libmproxy/console/connlist.py +++ b/libmproxy/console/connlist.py @@ -1,6 +1,61 @@ import urwid import common +class EventListBox(urwid.ListBox): + def __init__(self, master): + self.master = master + urwid.ListBox.__init__(self, master.eventlist) + + def keypress(self, size, key): + key = common.shortcuts(key) + if key == "C": + self.master.clear_events() + key = None + return urwid.ListBox.keypress(self, size, key) + + +class BodyPile(urwid.Pile): + def __init__(self, master): + h = urwid.Text("Event log") + h = urwid.Padding(h, align="left", width=("relative", 100)) + + self.inactive_header = urwid.AttrWrap(h, "inactive_heading") + self.active_header = urwid.AttrWrap(h, "heading") + + urwid.Pile.__init__( + self, + [ + ConnectionListBox(master), + urwid.Frame(EventListBox(master), header = self.inactive_header) + ] + ) + self.master = master + self.focus = 0 + + def keypress(self, size, key): + if key == "tab": + self.focus = (self.focus + 1)%len(self.widget_list) + self.set_focus(self.focus) + if self.focus == 1: + self.widget_list[1].header = self.active_header + else: + self.widget_list[1].header = self.inactive_header + key = None + elif key == "v": + self.master.toggle_eventlog() + key = None + + # This is essentially a copypasta from urwid.Pile's keypress handler. + # So much for "closed for modification, but open for extension". + item_rows = None + if len(size)==2: + item_rows = self.get_item_rows( size, focus=True ) + i = self.widget_list.index(self.focus_item) + tsize = self.get_item_size(size,i,True,item_rows) + return self.focus_item.keypress( tsize, key ) + + + class ConnectionItem(common.WWrap): def __init__(self, master, state, flow, focus): self.master, self.state, self.flow = master, state, flow diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py index 1e9d0c60..f4f6c662 100644 --- a/libmproxy/console/connview.py +++ b/libmproxy/console/connview.py @@ -370,9 +370,28 @@ class ConnectionView(common.WWrap): self.master.prompt_edit("Message", conn.msg, self.set_resp_msg) self.master.refresh_connection(self.flow) + def _view_nextprev_flow(self, np, flow): + try: + idx = self.state.view.index(flow) + except IndexError: + return + if np == "next": + new_flow, new_idx = self.state.get_next(idx) + else: + new_flow, new_idx = self.state.get_prev(idx) + if new_idx is None: + return + self.master.view_flow(new_flow) + + def view_next_flow(self, flow): + return self._view_nextprev_flow("next", flow) + + def view_prev_flow(self, flow): + return self._view_nextprev_flow("prev", flow) + def keypress(self, size, key): if key == " ": - self.master.view_next_flow(self.flow) + self.view_next_flow(self.flow) return key key = common.shortcuts(key) @@ -423,7 +442,7 @@ class ConnectionView(common.WWrap): ) key = None elif key == "p": - self.master.view_prev_flow(self.flow) + self.view_prev_flow(self.flow) elif key == "r": r = self.master.replay_request(self.flow) if r: diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index 7f2d965c..d9aaadce 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -1,70 +1,147 @@ +import time import urwid import common class SText(common.WWrap): - def __init__(self, txt): + def __init__(self, txt, focused): w = urwid.Text(txt, wrap="any") - w = urwid.AttrWrap(w, "editfield") + if focused: + w = urwid.AttrWrap(w, "editfield") common.WWrap.__init__(self, w) def keypress(self, size, key): - raise ValueError, key - time.sleep(0.5) return key def selectable(self): return True +class SEdit(common.WWrap): + def __init__(self, txt): + w = urwid.Edit(txt, wrap="any") + common.WWrap.__init__(self, w) + + def selectable(self): + return True + + +class KVItem(common.WWrap): + def __init__(self, focused, editing, maxk, k, v): + self.focused, self.editing = focused, editing + if focused == 0 and editing: + self.kf = SEdit(k) + else: + self.kf = SText(k, True if focused == 0 else False) + + if focused == 1 and editing: + self.vf = SEdit(v) + else: + self.vf = SText(v, True if focused == 1 else False) + + w = urwid.Columns( + [ + ("fixed", maxk + 2, self.kf), + self.vf + ], + dividechars = 2 + ) + if focused is not None: + w.set_focus_column(focused) + common.WWrap.__init__(self, w) + + def keypress(self, s, k): + if self.editing: + if self.focused == 0: + return self.kf.keypress(s, k) + else: + return self.vf.keypress(s, k) + return k + + def selectable(self): + return True + + +class KVWalker(urwid.ListWalker): + def __init__(self, lst): + self.lst = lst + self.maxk = max(len(v[0]) for v in lst) + self.focus = 0 + self.focus_col = 0 + self.editing = False + + def edit(self): + self.editing = KVItem(self.focus_col, True, self.maxk, *self.lst[self.focus]) + self._modified() + + def left(self): + self.focus_col = 0 + self._modified() + + def right(self): + self.focus_col = 1 + self._modified() + + def tab_next(self): + if self.focus_col == 0: + self.focus_col = 1 + elif self.focus != len(self.lst)-1: + self.focus_col = 0 + self.focus += 1 + self._modified() + + def get_focus(self): + if self.editing: + return self.editing, self.focus + else: + return KVItem(self.focus_col, False, self.maxk, *self.lst[self.focus]), self.focus + + def set_focus(self, focus): + self.focus = focus + + def get_next(self, pos): + if pos+1 >= len(self.lst): + return None, None + return KVItem(None, False, self.maxk, *self.lst[pos+1]), pos+1 + + def get_prev(self, pos): + if pos-1 < 0: + return None, None + return KVItem(None, False, self.maxk, *self.lst[pos-1]), pos-1 + + +class KVListBox(urwid.ListBox): + def __init__(self, lw): + urwid.ListBox.__init__(self, lw) + + class KVEditor(common.WWrap): def __init__(self, master, title, value, callback): self.master, self.title, self.value, self.callback = master, title, value, callback p = urwid.Text(title) p = urwid.Padding(p, align="left", width=("relative", 100)) p = urwid.AttrWrap(p, "heading") - maxk = max(len(v[0]) for v in value) - parts = [] - for k, v in value: - parts.append( - urwid.Columns( - [ - ( - "fixed", - maxk + 2, - SText(k) - ), - SText(v) - ], - dividechars = 2 - ) - ) - parts.append(urwid.Text(" ")) - self.lb = urwid.ListBox(parts) + self.walker = KVWalker(self.value) + self.lb = KVListBox(self.walker) self.w = urwid.Frame(self.lb, header = p) self.master.statusbar.update("") def keypress(self, size, key): - if key == "q": - self.master.pop_view() + if self.walker.editing: + self.w.keypress(size, key) return None - if key in ("tab", "enter"): - cw = self.lb.get_focus()[0] - col = cw.get_focus_column() - if col == 0: - cw.set_focus_column(1) + else: + key = common.shortcuts(key) + if key == "q": + self.master.pop_view() + elif key == "h": + self.walker.left() + elif key == "l": + self.walker.right() + elif key == "tab": + self.walker.tab_next() + elif key == "enter": + self.walker.edit() + elif key == "esc": + self.master.view_connlist() else: - self.lb._keypress_down(size) - cw = self.lb.get_focus()[0] - cw.set_focus_column(0) - return None - elif key == "ctrl e": - # Editor - pass - elif key == "ctrl r": - # Revert - pass - elif key == "esc": - self.master.view_connlist() - return - return self.w.keypress(size, key) - + return self.w.keypress(size, key) -- cgit v1.2.3 From 64360f5996ba1a9fa5dd596599d511257da8f6a4 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 14:58:48 +1300 Subject: Editing now works. --- libmproxy/console/kveditor.py | 65 ++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 25 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index d9aaadce..6d708de0 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -9,6 +9,9 @@ class SText(common.WWrap): w = urwid.AttrWrap(w, "editfield") common.WWrap.__init__(self, w) + def get_text(self): + return self.w.get_text()[0] + def keypress(self, size, key): return key @@ -18,9 +21,12 @@ class SText(common.WWrap): class SEdit(common.WWrap): def __init__(self, txt): - w = urwid.Edit(txt, wrap="any") + w = urwid.Edit(txt, wrap="any", multiline=True) common.WWrap.__init__(self, w) + def get_text(self): + return self.w.get_text()[0] + def selectable(self): return True @@ -29,12 +35,12 @@ class KVItem(common.WWrap): def __init__(self, focused, editing, maxk, k, v): self.focused, self.editing = focused, editing if focused == 0 and editing: - self.kf = SEdit(k) + self.editing = self.kf = SEdit(k) else: self.kf = SText(k, True if focused == 0 else False) if focused == 1 and editing: - self.vf = SEdit(v) + self.editing = self.vf = SEdit(v) else: self.vf = SText(v, True if focused == 1 else False) @@ -49,12 +55,12 @@ class KVItem(common.WWrap): w.set_focus_column(focused) common.WWrap.__init__(self, w) + def get_kv(self): + return (self.kf.get_text(), self.vf.get_text()) + def keypress(self, s, k): if self.editing: - if self.focused == 0: - return self.kf.keypress(s, k) - else: - return self.vf.keypress(s, k) + k = self.editing.keypress(s, k) return k def selectable(self): @@ -69,10 +75,15 @@ class KVWalker(urwid.ListWalker): self.focus_col = 0 self.editing = False - def edit(self): + def start_edit(self): self.editing = KVItem(self.focus_col, True, self.maxk, *self.lst[self.focus]) self._modified() + def stop_edit(self): + self.lst[self.focus] = self.editing.get_kv() + self.editing = False + self._modified() + def left(self): self.focus_col = 0 self._modified() @@ -127,21 +138,25 @@ class KVEditor(common.WWrap): def keypress(self, size, key): if self.walker.editing: - self.w.keypress(size, key) - return None - else: - key = common.shortcuts(key) - if key == "q": - self.master.pop_view() - elif key == "h": - self.walker.left() - elif key == "l": - self.walker.right() - elif key == "tab": - self.walker.tab_next() - elif key == "enter": - self.walker.edit() - elif key == "esc": - self.master.view_connlist() + if key in ["esc"]: + self.walker.stop_edit() + return None + elif key in ["left", "right", "up", "down"]: + self.walker.stop_edit() else: - return self.w.keypress(size, key) + self.w.keypress(size, key) + return None + + key = common.shortcuts(key) + if key == ["q", "esc"]: + self.master.pop_view() + elif key in ["h", "left"]: + self.walker.left() + elif key in ["l", "right"]: + self.walker.right() + elif key == "tab": + self.walker.tab_next() + elif key == "enter": + self.walker.start_edit() + else: + return self.w.keypress(size, key) -- cgit v1.2.3 From 8b5e0812333db79e10abef91a690932bce15511f Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 16:43:11 +1300 Subject: Refine look and feel, make editor operate on copy of data. --- libmproxy/console/__init__.py | 5 +++-- libmproxy/console/connview.py | 12 +++++------- libmproxy/console/kveditor.py | 35 +++++++++++++++++++---------------- 3 files changed, 27 insertions(+), 25 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 6fc76160..3e98ed2d 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -486,6 +486,7 @@ class ConsoleMaster(flow.FlowMaster): ('offset', 'dark cyan', 'default'), # KV Editor + ('focusfield', 'dark cyan', 'default'), ('editfield', 'black', 'light cyan'), ] @@ -536,9 +537,9 @@ class ConsoleMaster(flow.FlowMaster): self.header = None self.make_view() - def view_kveditor(self, title, value, callback): + def view_kveditor(self, title, value, callback, *args, **kwargs): self.statusbar = StatusBar(self, "foo") - self.body = kveditor.KVEditor(self, title, value, callback) + self.body = kveditor.KVEditor(self, title, value, callback, *args, **kwargs) self.header = None self.make_view() diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py index f4f6c662..ac25ea9a 100644 --- a/libmproxy/console/connview.py +++ b/libmproxy/console/connview.py @@ -1,6 +1,6 @@ import urwid import common -from .. import utils, encoding +from .. import utils, encoding, flow VIEW_CUTOFF = 1024*100 @@ -341,6 +341,9 @@ class ConnectionView(common.WWrap): response.msg = msg self.master.refresh_connection(self.flow) + def set_headers(self, lst, conn): + conn.headers = flow.Headers(lst) + def edit(self, part): if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: conn = self.flow.request @@ -354,12 +357,7 @@ class ConnectionView(common.WWrap): c = self._spawn_editor(conn.content or "") conn.content = c.rstrip("\n") elif part == "h": - self.master.view_kveditor("Editing headers", conn.headers.lst, None) - #headertext = self._spawn_editor(repr(conn.headers)) - #headers = flow.Headers() - #fp = cStringIO.StringIO(headertext) - #headers.read(fp) - #conn.headers = headers + self.master.view_kveditor("Editing headers", conn.headers.lst, self.set_headers, conn) elif part == "u" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: self.master.prompt_edit("URL", conn.get_url(), self.set_url) elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index 6d708de0..dae303e9 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -1,4 +1,4 @@ -import time +import copy import urwid import common @@ -6,7 +6,7 @@ class SText(common.WWrap): def __init__(self, txt, focused): w = urwid.Text(txt, wrap="any") if focused: - w = urwid.AttrWrap(w, "editfield") + w = urwid.AttrWrap(w, "focusfield") common.WWrap.__init__(self, w) def get_text(self): @@ -21,7 +21,8 @@ class SText(common.WWrap): class SEdit(common.WWrap): def __init__(self, txt): - w = urwid.Edit(txt, wrap="any", multiline=True) + w = urwid.Edit(edit_text=txt, wrap="any", multiline=True) + w = urwid.AttrWrap(w, "editfield") common.WWrap.__init__(self, w) def get_text(self): @@ -33,7 +34,7 @@ class SEdit(common.WWrap): class KVItem(common.WWrap): def __init__(self, focused, editing, maxk, k, v): - self.focused, self.editing = focused, editing + self.focused, self.editing, self.maxk = focused, editing, maxk if focused == 0 and editing: self.editing = self.kf = SEdit(k) else: @@ -60,7 +61,7 @@ class KVItem(common.WWrap): def keypress(self, s, k): if self.editing: - k = self.editing.keypress(s, k) + k = self.editing.keypress((s[0]-self.maxk-4,), k) return k def selectable(self): @@ -80,9 +81,10 @@ class KVWalker(urwid.ListWalker): self._modified() def stop_edit(self): - self.lst[self.focus] = self.editing.get_kv() - self.editing = False - self._modified() + if self.editing: + self.lst[self.focus] = self.editing.get_kv() + self.editing = False + self._modified() def left(self): self.focus_col = 0 @@ -107,6 +109,7 @@ class KVWalker(urwid.ListWalker): return KVItem(self.focus_col, False, self.maxk, *self.lst[self.focus]), self.focus def set_focus(self, focus): + self.stop_edit() self.focus = focus def get_next(self, pos): @@ -126,8 +129,10 @@ class KVListBox(urwid.ListBox): class KVEditor(common.WWrap): - def __init__(self, master, title, value, callback): + def __init__(self, master, title, value, callback, *cb_args, **cb_kwargs): + value = copy.deepcopy(value) self.master, self.title, self.value, self.callback = master, title, value, callback + self.cb_args, self.cb_kwargs = cb_args, cb_kwargs p = urwid.Text(title) p = urwid.Padding(p, align="left", width=("relative", 100)) p = urwid.AttrWrap(p, "heading") @@ -138,17 +143,15 @@ class KVEditor(common.WWrap): def keypress(self, size, key): if self.walker.editing: - if key in ["esc"]: - self.walker.stop_edit() - return None - elif key in ["left", "right", "up", "down"]: + if key in ["esc", "enter"]: self.walker.stop_edit() else: self.w.keypress(size, key) - return None + return None key = common.shortcuts(key) - if key == ["q", "esc"]: + if key in ["q", "esc"]: + self.callback(self.walker.lst, *self.cb_args, **self.cb_kwargs) self.master.pop_view() elif key in ["h", "left"]: self.walker.left() @@ -156,7 +159,7 @@ class KVEditor(common.WWrap): self.walker.right() elif key == "tab": self.walker.tab_next() - elif key == "enter": + elif key in ["enter", "e"]: self.walker.start_edit() else: return self.w.keypress(size, key) -- cgit v1.2.3 From 2b6bedac0ef5a79972dbb3042cc81b6dccd87fdc Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 16:55:11 +1300 Subject: Add and delete for KV editor. --- libmproxy/console/__init__.py | 2 +- libmproxy/console/kveditor.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 3e98ed2d..30625ec3 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -486,7 +486,7 @@ class ConsoleMaster(flow.FlowMaster): ('offset', 'dark cyan', 'default'), # KV Editor - ('focusfield', 'dark cyan', 'default'), + ('focusfield', 'white', 'dark blue'), ('editfield', 'black', 'light cyan'), ] diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index dae303e9..9b725590 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -76,6 +76,18 @@ class KVWalker(urwid.ListWalker): self.focus_col = 0 self.editing = False + def delete_focus(self): + if self.lst: + del self.lst[self.focus] + self.focus = min(len(self.lst)-1, self.focus) + self._modified() + + def add(self): + self.focus = min(self.focus +1, len(self.lst)) + self.lst.insert(self.focus, ("", "")) + self.focus_col = 0 + self.start_edit() + def start_edit(self): self.editing = KVItem(self.focus_col, True, self.maxk, *self.lst[self.focus]) self._modified() @@ -95,6 +107,7 @@ class KVWalker(urwid.ListWalker): self._modified() def tab_next(self): + self.stop_edit() if self.focus_col == 0: self.focus_col = 1 elif self.focus != len(self.lst)-1: @@ -105,8 +118,10 @@ class KVWalker(urwid.ListWalker): def get_focus(self): if self.editing: return self.editing, self.focus - else: + elif self.lst: return KVItem(self.focus_col, False, self.maxk, *self.lst[self.focus]), self.focus + else: + return None, None def set_focus(self, focus): self.stop_edit() @@ -145,6 +160,9 @@ class KVEditor(common.WWrap): if self.walker.editing: if key in ["esc", "enter"]: self.walker.stop_edit() + elif key == "tab": + self.walker.tab_next() + self.walker.start_edit() else: self.w.keypress(size, key) return None @@ -159,6 +177,10 @@ class KVEditor(common.WWrap): self.walker.right() elif key == "tab": self.walker.tab_next() + elif key == "a": + self.walker.add() + elif key == "d": + self.walker.delete_focus() elif key in ["enter", "e"]: self.walker.start_edit() else: -- cgit v1.2.3 From d41095c35ee49bf85a31a1830638fcc30afbe28e Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 17:52:43 +1300 Subject: "i" shortcut to insert for KVEditor. --- libmproxy/console/__init__.py | 2 +- libmproxy/console/kveditor.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 30625ec3..1b0ab35f 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -486,7 +486,7 @@ class ConsoleMaster(flow.FlowMaster): ('offset', 'dark cyan', 'default'), # KV Editor - ('focusfield', 'white', 'dark blue'), + ('focusfield', 'black', 'light gray'), ('editfield', 'black', 'light cyan'), ] diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index 9b725590..95b703be 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -82,12 +82,18 @@ class KVWalker(urwid.ListWalker): self.focus = min(len(self.lst)-1, self.focus) self._modified() - def add(self): - self.focus = min(self.focus +1, len(self.lst)) + def _insert(self, pos): + self.focus = pos self.lst.insert(self.focus, ("", "")) self.focus_col = 0 self.start_edit() + def insert(self): + return self._insert(self.focus) + + def add(self): + return self._insert(min(self.focus + 1, len(self.lst))) + def start_edit(self): self.editing = KVItem(self.focus_col, True, self.maxk, *self.lst[self.focus]) self._modified() @@ -179,6 +185,8 @@ class KVEditor(common.WWrap): self.walker.tab_next() elif key == "a": self.walker.add() + elif key == "i": + self.walker.insert() elif key == "d": self.walker.delete_focus() elif key in ["enter", "e"]: -- cgit v1.2.3 From 4026aa2e5f6929633b4800d55cf26698f9dd3c40 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 17:55:17 +1300 Subject: KVEditor: make tab behaviour nicer If we tab while editing, stop editing if we are taken to the next row. --- libmproxy/console/kveditor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index 95b703be..40387a56 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -167,8 +167,10 @@ class KVEditor(common.WWrap): if key in ["esc", "enter"]: self.walker.stop_edit() elif key == "tab": + pf = self.walker.focus self.walker.tab_next() - self.walker.start_edit() + if self.walker.focus == pf: + self.walker.start_edit() else: self.w.keypress(size, key) return None -- cgit v1.2.3 From 76f2595df76f81bdbca360fe032167b6b1269a4d Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 18:25:00 +1300 Subject: KVEditor: "e" shortcut spawns an external editor on a field. --- libmproxy/console/__init__.py | 22 ++++++++++++++++++++++ libmproxy/console/connview.py | 24 +----------------------- libmproxy/console/kveditor.py | 19 ++++++++++++++++++- 3 files changed, 41 insertions(+), 24 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 1b0ab35f..cbc789f6 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -426,6 +426,28 @@ class ConsoleMaster(flow.FlowMaster): False ) + def spawn_editor(self, data): + fd, name = tempfile.mkstemp('', "mproxy") + os.write(fd, data) + os.close(fd) + c = os.environ.get("EDITOR") + #If no EDITOR is set, assume 'vi' + if not c: + c = "vi" + cmd = [c, name] + self.ui.stop() + try: + subprocess.call(cmd) + except: + self.statusbar.message("Can't start editor: %s" % c) + self.ui.start() + os.unlink(name) + return data + self.ui.start() + data = open(name).read() + os.unlink(name) + return data + def spawn_external_viewer(self, data, contenttype): if contenttype: ext = mimetypes.guess_extension(contenttype) or "" diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py index ac25ea9a..0b354479 100644 --- a/libmproxy/console/connview.py +++ b/libmproxy/console/connview.py @@ -275,28 +275,6 @@ class ConnectionView(common.WWrap): else: self.view_request() - def _spawn_editor(self, data): - fd, name = tempfile.mkstemp('', "mproxy") - os.write(fd, data) - os.close(fd) - c = os.environ.get("EDITOR") - #If no EDITOR is set, assume 'vi' - if not c: - c = "vi" - cmd = [c, name] - self.master.ui.stop() - try: - subprocess.call(cmd) - except: - self.master.statusbar.message("Can't start editor: %s" % c) - self.master.ui.start() - os.unlink(name) - return data - self.master.ui.start() - data = open(name).read() - os.unlink(name) - return data - def edit_method(self, m): for i in self.methods: if i[1] == m: @@ -354,7 +332,7 @@ class ConnectionView(common.WWrap): self.flow.backup() if part == "b": - c = self._spawn_editor(conn.content or "") + c = self.master.spawn_editor(conn.content or "") conn.content = c.rstrip("\n") elif part == "h": self.master.view_kveditor("Editing headers", conn.headers.lst, self.set_headers, conn) diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index 40387a56..ceed2e00 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -1,6 +1,7 @@ import copy import urwid import common +from .. import utils class SText(common.WWrap): def __init__(self, txt, focused): @@ -76,6 +77,15 @@ class KVWalker(urwid.ListWalker): self.focus_col = 0 self.editing = False + def get_current_value(self): + if self.lst: + return self.lst[self.focus][self.focus_col] + + def set_current_value(self, val): + row = list(self.lst[self.focus]) + row[self.focus_col] = val + self.lst[self.focus] = tuple(row) + def delete_focus(self): if self.lst: del self.lst[self.focus] @@ -174,7 +184,7 @@ class KVEditor(common.WWrap): else: self.w.keypress(size, key) return None - + key = common.shortcuts(key) if key in ["q", "esc"]: self.callback(self.walker.lst, *self.cb_args, **self.cb_kwargs) @@ -191,6 +201,13 @@ class KVEditor(common.WWrap): self.walker.insert() elif key == "d": self.walker.delete_focus() + elif key == "e": + o = self.walker.get_current_value() + if o is not None: + n = self.master.spawn_editor(o) + n = utils.clean_hanging_newline(n) + self.walker.set_current_value(n) + self.walker._modified() elif key in ["enter", "e"]: self.walker.start_edit() else: -- cgit v1.2.3 From e3f28e1c06093147660e2857adce24b441d6530f Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 21:47:39 +1300 Subject: Move to context-dependent help model. The all-in-one page was just getting too unwieldy. --- libmproxy/console/__init__.py | 30 ++++++++++++++++++++---------- libmproxy/console/connlist.py | 15 +++++++++++++++ libmproxy/console/connview.py | 16 ++++++++++++++++ libmproxy/console/help.py | 38 +++++++++++--------------------------- libmproxy/console/kveditor.py | 20 +++++++++++++++++++- 5 files changed, 81 insertions(+), 38 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index cbc789f6..196e71c7 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -523,9 +523,11 @@ class ConsoleMaster(flow.FlowMaster): self.statusbar = None self.header = None self.body = None + self.help_context = None self.prompting = False self.onekey = False + self.view_connlist() if self.server: @@ -545,6 +547,15 @@ class ConsoleMaster(flow.FlowMaster): sys.stderr.flush() self.shutdown() + def focus_current(self): + if self.currentflow: + try: + ids = [id(i) for i in self.state.view] + idx = ids.index(id(self.currentflow)) + self.conn_list_view.set_focus(idx) + except (IndexError, ValueError): + pass + def make_view(self): self.view = urwid.Frame( self.body, @@ -554,8 +565,10 @@ class ConsoleMaster(flow.FlowMaster): self.view.set_focus("body") def view_help(self): + h = help.HelpView(self, self.help_context, (self.statusbar, self.body, self.header)) + self.statusbar = StatusBar(self, self.footer_text_help) - self.body = help.HelpView(self) + self.body = h self.header = None self.make_view() @@ -563,16 +576,9 @@ class ConsoleMaster(flow.FlowMaster): self.statusbar = StatusBar(self, "foo") self.body = kveditor.KVEditor(self, title, value, callback, *args, **kwargs) self.header = None - self.make_view() - def focus_current(self): - if self.currentflow: - try: - ids = [id(i) for i in self.state.view] - idx = ids.index(id(self.currentflow)) - self.conn_list_view.set_focus(idx) - except (IndexError, ValueError): - pass + self.help_context = kveditor.help_context + self.make_view() def view_connlist(self): if self.ui.started: @@ -585,14 +591,18 @@ class ConsoleMaster(flow.FlowMaster): self.statusbar = StatusBar(self, self.footer_text_default) self.header = None self.currentflow = None + self.make_view() + self.help_context = connlist.help_context def view_flow(self, flow): self.statusbar = StatusBar(self, self.footer_text_connview) self.body = connview.ConnectionView(self, self.state, flow) self.header = connview.ConnectionViewHeader(self, flow) self.currentflow = flow + self.make_view() + self.help_context = connview.help_context def _write_flows(self, path, flows): self.state.last_saveload = path diff --git a/libmproxy/console/connlist.py b/libmproxy/console/connlist.py index 319963e8..1d34fba9 100644 --- a/libmproxy/console/connlist.py +++ b/libmproxy/console/connlist.py @@ -1,6 +1,21 @@ import urwid import common +def _mkhelp(): + text = [] + keys = [ + ("C", "clear connection list or eventlog"), + ("d", "delete connection from view"), + ("v", "toggle eventlog"), + ("X", "kill and delete connection, even if it's mid-intercept"), + ("tab", "tab between eventlog and connection list"), + ("enter", "view connection"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + class EventListBox(urwid.ListBox): def __init__(self, master): self.master = master diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py index 0b354479..f6d24d5e 100644 --- a/libmproxy/console/connview.py +++ b/libmproxy/console/connview.py @@ -2,6 +2,22 @@ import urwid import common from .. import utils, encoding, flow +def _mkhelp(): + text = [] + keys = [ + ("b", "save request/response body"), + ("e", "edit request/response"), + ("p", "previous flow"), + ("v", "view body in external viewer"), + ("z", "encode/decode a request/response"), + ("tab", "toggle request/response view"), + ("space", "next flow"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + VIEW_CUTOFF = 1024*100 class ConnectionViewHeader(common.WWrap): diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 574dd7ca..b2eafbf0 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -3,8 +3,9 @@ import common from .. import filt class HelpView(urwid.ListBox): - def __init__(self, master): - self.master = master + def __init__(self, master, help_context, state): + self.master, self.state = master, state + self.help_context = help_context or [] urwid.ListBox.__init__( self, self.helptext() @@ -13,13 +14,19 @@ class HelpView(urwid.ListBox): def keypress(self, size, key): key = common.shortcuts(key) if key == "q": - self.master.pop_view() + self.master.statusbar = self.state[0] + self.master.body = self.state[1] + self.master.header = self.state[2] + self.master.make_view() return None return urwid.ListBox.keypress(self, size, key) def helptext(self): text = [] - text.append(("head", "Global keys:\n")) + text.append(("head", "Keys for this view:\n")) + text.extend(self.help_context) + + text.append(("head", "\n\nGlobal keys:\n")) keys = [ ("A", "accept all intercepted connections"), ("a", "accept this intercepted connection"), @@ -77,29 +84,6 @@ class HelpView(urwid.ListBox): ] text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) - text.append(("head", "\n\nConnection list keys:\n")) - keys = [ - ("C", "clear connection list or eventlog"), - ("d", "delete connection from view"), - ("v", "toggle eventlog"), - ("X", "kill and delete connection, even if it's mid-intercept"), - ("tab", "tab between eventlog and connection list"), - ("enter", "view connection"), - ] - text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) - - text.append(("head", "\n\nConnection view keys:\n")) - keys = [ - ("b", "save request/response body"), - ("e", "edit request/response"), - ("p", "previous flow"), - ("v", "view body in external viewer"), - ("z", "encode/decode a request/response"), - ("tab", "toggle request/response view"), - ("space", "next flow"), - ] - text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) - text.append(("head", "\n\nFilter expressions:\n")) f = [] for i in filt.filt_unary: diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index ceed2e00..7afcd08d 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -3,6 +3,24 @@ import urwid import common from .. import utils + +def _mkhelp(): + text = [] + keys = [ + ("a", "add row after cursor"), + ("d", "delete row"), + ("e", "spawn external editor on current field"), + ("i", "insert row before cursor"), + ("q", "return to flow view"), + ("esc", "return to flow view/exit field edit mode"), + ("tab", "next field"), + ("enter", "edit field"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + return text +help_context = _mkhelp() + + class SText(common.WWrap): def __init__(self, txt, focused): w = urwid.Text(txt, wrap="any") @@ -208,7 +226,7 @@ class KVEditor(common.WWrap): n = utils.clean_hanging_newline(n) self.walker.set_current_value(n) self.walker._modified() - elif key in ["enter", "e"]: + elif key in ["enter"]: self.walker.start_edit() else: return self.w.keypress(size, key) -- cgit v1.2.3 From 866a93a8bc28fed47dde04f49c13592a7163bff4 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 22:28:15 +1300 Subject: Start consolidating keybindings. I want each view to have a more coherent set of bindings. This means minimizing the global bindings, and making some bindings accessible only from screens related to their functionality. --- libmproxy/console/__init__.py | 14 +++----------- libmproxy/console/connlist.py | 12 +++++++++--- libmproxy/console/connview.py | 30 +++++++++++++++++++++++++++++- libmproxy/console/help.py | 17 ----------------- 4 files changed, 41 insertions(+), 32 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 196e71c7..69a50f18 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -372,11 +372,13 @@ class ConsoleMaster(flow.FlowMaster): sys.exit(1) def run_script_once(self, path, f): + if not path: + return ret = self.get_script(path) if ret[0]: self.statusbar.message(ret[0]) + return s = ret[1] - if f.request: s.run("request", f) if f.response: @@ -781,16 +783,6 @@ class ConsoleMaster(flow.FlowMaster): self.set_intercept ) self.sync_list_view() - elif k == "m": - self.prompt_onekey( - "View", - ( - ("raw", "r"), - ("pretty", "p"), - ("hex", "h"), - ), - self.changeview - ) elif k == "Q": raise Stop elif k == "q": diff --git a/libmproxy/console/connlist.py b/libmproxy/console/connlist.py index 1d34fba9..d9ef258b 100644 --- a/libmproxy/console/connlist.py +++ b/libmproxy/console/connlist.py @@ -4,12 +4,15 @@ import common def _mkhelp(): text = [] keys = [ + ("A", "accept all intercepted connections"), + ("a", "accept this intercepted connection"), ("C", "clear connection list or eventlog"), ("d", "delete connection from view"), ("v", "toggle eventlog"), ("X", "kill and delete connection, even if it's mid-intercept"), ("tab", "tab between eventlog and connection list"), ("enter", "view connection"), + ("|", "run script on this flow"), ] text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) return text @@ -117,10 +120,13 @@ class ConnectionItem(common.WWrap): self.master.view_flow(self.flow) elif key == "|": self.master.path_prompt( - "Send flow to script: ", self.state.last_script, - self.master.run_script_once, self.flow + "Send flow to script: ", + self.state.last_script, + self.master.run_script_once, + self.flow ) - return key + else: + return key class ConnectionListView(urwid.ListWalker): diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py index f6d24d5e..49418d01 100644 --- a/libmproxy/console/connview.py +++ b/libmproxy/console/connview.py @@ -5,13 +5,29 @@ from .. import utils, encoding, flow def _mkhelp(): text = [] keys = [ + ("A", "accept all intercepted connections"), + ("a", "accept this intercepted connection"), ("b", "save request/response body"), ("e", "edit request/response"), + ("m", "change body display mode"), + (None, + common.highlight_key("raw", "r") + + [("text", ": raw data")] + ), + (None, + common.highlight_key("pretty", "p") + + [("text", ": pretty-print XML, HTML and JSON")] + ), + (None, + common.highlight_key("hex", "h") + + [("text", ": hex dump")] + ), ("p", "previous flow"), ("v", "view body in external viewer"), ("z", "encode/decode a request/response"), ("tab", "toggle request/response view"), ("space", "next flow"), + ("|", "run script on this flow"), ] text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) return text @@ -433,6 +449,17 @@ class ConnectionView(common.WWrap): self.edit ) key = None + elif key == "m": + self.master.prompt_onekey( + "View", + ( + ("raw", "r"), + ("pretty", "p"), + ("hex", "h"), + ), + self.master.changeview + ) + key = None elif key == "p": self.view_prev_flow(self.flow) elif key == "r": @@ -490,7 +517,8 @@ class ConnectionView(common.WWrap): conn ) self.master.refresh_connection(self.flow) - return key + else: + return key def encode_callback(self, key, conn): encoding_map = { diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index b2eafbf0..0c710c7b 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -28,28 +28,12 @@ class HelpView(urwid.ListBox): text.append(("head", "\n\nGlobal keys:\n")) keys = [ - ("A", "accept all intercepted connections"), - ("a", "accept this intercepted connection"), ("c", "client replay"), ("i", "set interception pattern"), ("j, k", "up, down"), ("l", "set limit filter pattern"), ("L", "load saved flows"), - ("m", "change body display mode"), - (None, - common.highlight_key("raw", "r") + - [("text", ": raw data")] - ), - (None, - common.highlight_key("pretty", "p") + - [("text", ": pretty-print XML, HTML and JSON")] - ), - (None, - common.highlight_key("hex", "h") + - [("text", ": hex dump")] - ), - ("o", "toggle options:"), (None, common.highlight_key("anticache", "a") + @@ -78,7 +62,6 @@ class HelpView(urwid.ListBox): ("u", "set sticky auth expression"), ("w", "save all flows matching current limit"), ("W", "save this flow"), - ("|", "run script on this flow"), ("space", "page down"), ("pg up/down", "page up/down"), ] -- cgit v1.2.3 From 5df0b9e9610b803241f8c4870ddfe3afb450a048 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 8 Feb 2012 22:55:48 +1300 Subject: Further keybinding consolidation. Also, move KVEditor's "i" binding to "A" to avoid clashes with global bindings. --- libmproxy/console/__init__.py | 15 --------------- libmproxy/console/connlist.py | 25 +++++++++++++++++++++++-- libmproxy/console/connview.py | 4 ++++ libmproxy/console/help.py | 19 ++++++++++--------- libmproxy/console/kveditor.py | 4 ++-- 5 files changed, 39 insertions(+), 28 deletions(-) (limited to 'libmproxy/console') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 69a50f18..608742a4 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -773,9 +773,6 @@ class ConsoleMaster(flow.FlowMaster): ), self.stop_client_playback_prompt, ) - elif k == "l": - self.prompt("Limit: ", self.state.limit_txt, self.set_limit) - self.sync_list_view() elif k == "i": self.prompt( "Intercept filter: ", @@ -794,12 +791,6 @@ class ConsoleMaster(flow.FlowMaster): ), self.quit, ) - elif k == "w": - self.path_prompt( - "Save flows: ", - self.state.last_saveload, - self.save_flows - ) elif k == "s": if self.script: self.load_script(None) @@ -825,12 +816,6 @@ class ConsoleMaster(flow.FlowMaster): ), self.stop_server_playback_prompt, ) - elif k == "L": - self.path_prompt( - "Load flows: ", - self.state.last_saveload, - self.load_flows_callback - ) elif k == "o": self.prompt_onekey( "Options", diff --git a/libmproxy/console/connlist.py b/libmproxy/console/connlist.py index d9ef258b..0f238322 100644 --- a/libmproxy/console/connlist.py +++ b/libmproxy/console/connlist.py @@ -8,7 +8,13 @@ def _mkhelp(): ("a", "accept this intercepted connection"), ("C", "clear connection list or eventlog"), ("d", "delete connection from view"), + ("l", "set limit filter pattern"), + ("L", "load saved flows"), + ("r", "replay request"), + ("R", "revert changes to request"), ("v", "toggle eventlog"), + ("w", "save all flows matching current limit"), + ("W", "save this flow"), ("X", "kill and delete connection, even if it's mid-intercept"), ("tab", "tab between eventlog and connection list"), ("enter", "view connection"), @@ -96,6 +102,15 @@ class ConnectionItem(common.WWrap): self.flow.kill(self.master) self.state.delete_flow(self.flow) self.master.sync_list_view() + elif key == "l": + self.master.prompt("Limit: ", self.state.limit_txt, self.master.set_limit) + self.master.sync_list_view() + elif key == "L": + self.master.path_prompt( + "Load flows: ", + self.state.last_saveload, + self.master.load_flows_callback + ) elif key == "r": r = self.master.replay_request(self.flow) if r: @@ -104,6 +119,12 @@ class ConnectionItem(common.WWrap): elif key == "R": self.state.revert(self.flow) self.master.sync_list_view() + elif key == "w": + self.master.path_prompt( + "Save flows: ", + self.state.last_saveload, + self.master.save_flows + ) elif key == "W": self.master.path_prompt( "Save this flow: ", @@ -120,9 +141,9 @@ class ConnectionItem(common.WWrap): self.master.view_flow(self.flow) elif key == "|": self.master.path_prompt( - "Send flow to script: ", + "Send flow to script: ", self.state.last_script, - self.master.run_script_once, + self.master.run_script_once, self.flow ) else: diff --git a/libmproxy/console/connview.py b/libmproxy/console/connview.py index 49418d01..289d8024 100644 --- a/libmproxy/console/connview.py +++ b/libmproxy/console/connview.py @@ -23,7 +23,11 @@ def _mkhelp(): [("text", ": hex dump")] ), ("p", "previous flow"), + ("r", "replay request"), + ("R", "revert changes to request"), ("v", "view body in external viewer"), + ("w", "save all flows matching current limit"), + ("W", "save this flow"), ("z", "encode/decode a request/response"), ("tab", "toggle request/response view"), ("space", "next flow"), diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 0c710c7b..22b42475 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -26,13 +26,20 @@ class HelpView(urwid.ListBox): text.append(("head", "Keys for this view:\n")) text.extend(self.help_context) + text.append(("head", "\n\nMovement:\n")) + keys = [ + ("j, k", "up, down"), + ("h, l", "left, right (in some contexts)"), + ("space", "page down"), + ("pg up/down", "page up/down"), + ("arrows", "up, down, left, right"), + ] + text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) + text.append(("head", "\n\nGlobal keys:\n")) keys = [ ("c", "client replay"), ("i", "set interception pattern"), - ("j, k", "up, down"), - ("l", "set limit filter pattern"), - ("L", "load saved flows"), ("o", "toggle options:"), (None, @@ -54,16 +61,10 @@ class HelpView(urwid.ListBox): ("q", "quit / return to connection list"), ("Q", "quit without confirm prompt"), - ("r", "replay request"), - ("R", "revert changes to request"), ("s", "set/unset script"), ("S", "server replay"), ("t", "set sticky cookie expression"), ("u", "set sticky auth expression"), - ("w", "save all flows matching current limit"), - ("W", "save this flow"), - ("space", "page down"), - ("pg up/down", "page up/down"), ] text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) diff --git a/libmproxy/console/kveditor.py b/libmproxy/console/kveditor.py index 7afcd08d..632a6992 100644 --- a/libmproxy/console/kveditor.py +++ b/libmproxy/console/kveditor.py @@ -7,10 +7,10 @@ from .. import utils def _mkhelp(): text = [] keys = [ + ("A", "insert row before cursor"), ("a", "add row after cursor"), ("d", "delete row"), ("e", "spawn external editor on current field"), - ("i", "insert row before cursor"), ("q", "return to flow view"), ("esc", "return to flow view/exit field edit mode"), ("tab", "next field"), @@ -215,7 +215,7 @@ class KVEditor(common.WWrap): self.walker.tab_next() elif key == "a": self.walker.add() - elif key == "i": + elif key == "A": self.walker.insert() elif key == "d": self.walker.delete_focus() -- cgit v1.2.3