From 244ef243d75145a01d9029589de65be51299b3f3 Mon Sep 17 00:00:00 2001 From: Krzysztof Bielicki Date: Tue, 10 Mar 2015 10:44:06 +0100 Subject: [#514] Add support for ignoring payload params in multipart/form-data --- libmproxy/console/contentview.py | 24 ++---------------------- libmproxy/flow.py | 2 +- libmproxy/protocol/http.py | 21 ++++++++++++++++++++- libmproxy/utils.py | 27 +++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 24 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/contentview.py b/libmproxy/console/contentview.py index 582723bb..84e9946d 100644 --- a/libmproxy/console/contentview.py +++ b/libmproxy/console/contentview.py @@ -210,33 +210,13 @@ class ViewMultipart: prompt = ("multipart", "m") content_types = ["multipart/form-data"] def __call__(self, hdrs, content, limit): - v = hdrs.get_first("content-type") + v = utils.multipartdecode(hdrs, content) if v: - v = utils.parse_content_type(v) - if not v: - return - boundary = v[2].get("boundary") - if not boundary: - return - - 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(netlib.utils.cleanBin( - "\n".join(parts[3+parts[2:].index(""):]) - )) r = [ urwid.Text(("highlight", "Form data:\n")), ] r.extend(common.format_keyvals( - zip(keys, vals), + v, key = "header", val = "text" )) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 43580109..0e9e481c 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -236,7 +236,7 @@ class ServerPlaybackState: ] if not self.ignore_content: - form_contents = r.get_form_urlencoded() + form_contents = r.get_form() if self.ignore_payload_params and form_contents: key.extend( p for p in form_contents diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 49310ec3..512cf75b 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -15,6 +15,7 @@ from ..proxy.connection import ServerConnection from .. import encoding, utils, controller, stateobject, proxy HDR_FORM_URLENCODED = "application/x-www-form-urlencoded" +HDR_FORM_MULTIPART = "multipart/form-data" CONTENT_MISSING = 0 @@ -507,6 +508,19 @@ class HTTPRequest(HTTPMessage): """ self.headers["Host"] = [self.host] + def get_form(self): + """ + Retrieves the URL-encoded or multipart form data, returning an ODict object. + Returns an empty ODict if there is no data or the content-type + indicates non-form data. + """ + if self.content: + if self.headers.in_any("content-type", HDR_FORM_URLENCODED, True): + return self.get_form_urlencoded() + elif self.headers.in_any("content-type", HDR_FORM_MULTIPART, True): + return self.get_form_multipart() + return ODict([]) + def get_form_urlencoded(self): """ Retrieves the URL-encoded form data, returning an ODict object. @@ -514,7 +528,12 @@ class HTTPRequest(HTTPMessage): indicates non-form data. """ if self.content and self.headers.in_any("content-type", HDR_FORM_URLENCODED, True): - return ODict(utils.urldecode(self.content)) + return ODict(utils.urldecode(self.content)) + return ODict([]) + + def get_form_multipart(self): + if self.content and self.headers.in_any("content-type", HDR_FORM_MULTIPART, True): + return ODict(utils.multipartdecode(self.headers, self.content)) return ODict([]) def set_form_urlencoded(self, odict): diff --git a/libmproxy/utils.py b/libmproxy/utils.py index 51f2dc26..b84c589a 100644 --- a/libmproxy/utils.py +++ b/libmproxy/utils.py @@ -69,6 +69,33 @@ def urlencode(s): return urllib.urlencode(s, False) +def multipartdecode(hdrs, content): + """ + Takes a multipart boundary encoded string and returns list of (key, value) tuples. + """ + v = hdrs.get_first("content-type") + if v: + v = parse_content_type(v) + if not v: + return [] + boundary = v[2].get("boundary") + if not boundary: + return [] + + rx = re.compile(r'\bname="([^"]+)"') + r = [] + + 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: + key = match.group(1) + value = "".join(parts[3+parts[2:].index(""):]) + r.append((key, value)) + return r + return [] + def pretty_size(size): suffixes = [ ("B", 2**10), -- cgit v1.2.3 From d7e53e6573426c40ac7cfbaa7754380985227eb1 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 09:30:29 +1300 Subject: Fix crashes on mouse click when input is being handled --- libmproxy/console/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 198b7bbe..70b82d1d 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -617,8 +617,6 @@ class ConsoleMaster(flow.FlowMaster): self.prompt_execute(k) elif k == "enter": self.prompt_execute() - else: - self.view.keypress(self.loop.screen_size, k) else: k = self.view.keypress(self.loop.screen_size, k) if k: @@ -943,7 +941,7 @@ class ConsoleMaster(flow.FlowMaster): mkup.append(",") prompt.extend(mkup) prompt.append(")? ") - self.onekey = "".join(i[1] for i in keys) + self.onekey = set(i[1] for i in keys) self.prompt(prompt, "", callback, *args) def prompt_done(self): -- cgit v1.2.3 From a3f4296bf1ba0ac1a72d5a44a504d375707fdc39 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 10:02:34 +1300 Subject: Explicitly handle keyboard interrupt in mitmproxy Fixes #522 --- libmproxy/console/__init__.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 70b82d1d..9796677f 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -7,6 +7,7 @@ import tempfile import os import os.path import shlex +import signal import stat import subprocess import sys @@ -25,7 +26,8 @@ EVENTLOG_SIZE = 500 class _PathCompleter: def __init__(self, _testing=False): """ - _testing: disables reloading of the lookup table to make testing possible. + _testing: disables reloading of the lookup table to make testing + possible. """ self.lookup, self.offset = None, None self.final = None @@ -37,7 +39,8 @@ class _PathCompleter: def complete(self, txt): """ - Returns the next completion for txt, or None if there is no completion. + Returns the next completion for txt, or None if there is no + completion. """ path = os.path.expanduser(txt) if not self.lookup: @@ -702,14 +705,6 @@ class ConsoleMaster(flow.FlowMaster): self.edit_scripts ) ) - #if self.scripts: - # 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( @@ -799,6 +794,14 @@ class ConsoleMaster(flow.FlowMaster): sys.exit(1) self.loop.set_alarm_in(0.01, self.ticker) + + # It's not clear why we need to handle this explicitly - without this, + # mitmproxy hangs on keyboard interrupt. Remove if we ever figure it + # out. + def exit(s, f): + raise urwid.ExitMainLoop + signal.signal(signal.SIGINT, exit) + try: self.loop.run() except Exception: -- cgit v1.2.3 From 560e44c637e4f1fcbeba1305fc1eb39e3d796013 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 10:54:57 +1300 Subject: Pull PathEdit out into its own file. --- libmproxy/console/__init__.py | 69 ++----------------------------------------- libmproxy/console/pathedit.py | 69 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 67 deletions(-) create mode 100644 libmproxy/console/pathedit.py (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 9796677f..013c8003 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -18,76 +18,11 @@ import weakref from .. import controller, utils, flow, script from . import flowlist, flowview, help, common -from . import grideditor, palettes, contentview, flowdetailview +from . import grideditor, palettes, contentview, flowdetailview, pathedit EVENTLOG_SIZE = 500 -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(urwid.WidgetWrap): def __init__(self): self.message("") @@ -97,7 +32,7 @@ class ActionBar(urwid.WidgetWrap): def path_prompt(self, prompt, text): self.expire = None - self._w = PathEdit(prompt, text) + self._w = pathedit.PathEdit(prompt, text) def prompt(self, prompt, text = ""): self.expire = None diff --git a/libmproxy/console/pathedit.py b/libmproxy/console/pathedit.py new file mode 100644 index 00000000..53cda3be --- /dev/null +++ b/libmproxy/console/pathedit.py @@ -0,0 +1,69 @@ +import glob +import os.path + +import urwid + + +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) -- cgit v1.2.3 From 558e0a41c25ed927a3bd3244e82e50f2c1ec9f1c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 11:00:24 +1300 Subject: Fix general prompt input. --- libmproxy/console/__init__.py | 3 +++ libmproxy/console/common.py | 8 ++++++++ 2 files changed, 11 insertions(+) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 013c8003..5ff8e8d7 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -555,6 +555,9 @@ class ConsoleMaster(flow.FlowMaster): self.prompt_execute(k) elif k == "enter": self.prompt_execute() + else: + if common.is_keypress(k): + self.view.keypress(self.loop.screen_size, k) else: k = self.view.keypress(self.loop.screen_size, k) if k: diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 3a708c7c..90204d79 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -31,6 +31,14 @@ METHOD_OPTIONS = [ ] +def is_keypress(k): + """ + Is this input event a keypress? + """ + if isinstance(k, basestring): + return True + + def highlight_key(s, k): l = [] parts = s.split(k, 1) -- cgit v1.2.3 From 241530eb0aa69c5a69bed979a1a2a3a23d473112 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 11:03:46 +1300 Subject: Remove cruft to work around an old Urwid bug --- libmproxy/console/__init__.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 5ff8e8d7..f3c8ee12 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -36,11 +36,6 @@ class ActionBar(urwid.WidgetWrap): def prompt(self, prompt, text = ""): self.expire = None - # A (partial) workaround for this Urwid issue: - # https://github.com/Nic0/tyrs/issues/115 - # We can remove it once veryone is beyond 1.0.1 - if isinstance(prompt, basestring): - prompt = unicode(prompt) self._w = urwid.Edit(prompt, text or "") def message(self, message, expire=None): -- cgit v1.2.3 From 2f8ebfdce2165f1bd9196954a1d3bcdfec463494 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 11:08:04 +1300 Subject: Pull console StatusBar into its own file. --- libmproxy/console/__init__.py | 189 ++--------------------------------------- libmproxy/console/statusbar.py | 180 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 182 deletions(-) create mode 100644 libmproxy/console/statusbar.py (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index f3c8ee12..5f564a20 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -1,6 +1,5 @@ from __future__ import absolute_import -import glob import mailcap import mimetypes import tempfile @@ -11,191 +10,17 @@ import signal import stat import subprocess import sys -import time import traceback import urwid import weakref -from .. import controller, utils, flow, script +from .. import controller, flow, script from . import flowlist, flowview, help, common -from . import grideditor, palettes, contentview, flowdetailview, pathedit +from . import grideditor, palettes, contentview, flowdetailview, statusbar EVENTLOG_SIZE = 500 -class ActionBar(urwid.WidgetWrap): - def __init__(self): - self.message("") - - def selectable(self): - return True - - def path_prompt(self, prompt, text): - self.expire = None - self._w = pathedit.PathEdit(prompt, text) - - def prompt(self, prompt, text = ""): - self.expire = None - self._w = urwid.Edit(prompt, text or "") - - def message(self, message, expire=None): - self.expire = expire - self._w = urwid.Text(message) - - -class StatusBar(urwid.WidgetWrap): - def __init__(self, master, helptext): - self.master, self.helptext = master, helptext - self.ab = ActionBar() - self.ib = urwid.WidgetWrap(urwid.Text("")) - self._w = urwid.Pile([self.ib, self.ab]) - - def get_status(self): - r = [] - - if self.master.setheaders.count(): - r.append("[") - r.append(("heading_key", "H")) - r.append("eaders]") - if self.master.replacehooks.count(): - r.append("[") - r.append(("heading_key", "R")) - r.append("eplacing]") - if self.master.client_playback: - r.append("[") - r.append(("heading_key", "cplayback")) - r.append(":%s to go]"%self.master.client_playback.count()) - if self.master.server_playback: - r.append("[") - r.append(("heading_key", "splayback")) - if self.master.nopop: - r.append(":%s in file]"%self.master.server_playback.count()) - else: - r.append(":%s to go]"%self.master.server_playback.count()) - if self.master.get_ignore_filter(): - r.append("[") - r.append(("heading_key", "I")) - r.append("gnore:%d]" % len(self.master.get_ignore_filter())) - if self.master.get_tcp_filter(): - r.append("[") - r.append(("heading_key", "T")) - r.append("CP:%d]" % len(self.master.get_tcp_filter())) - if self.master.state.intercept_txt: - r.append("[") - r.append(("heading_key", "i")) - r.append(":%s]"%self.master.state.intercept_txt) - if self.master.state.limit_txt: - r.append("[") - r.append(("heading_key", "l")) - r.append(":%s]"%self.master.state.limit_txt) - if self.master.stickycookie_txt: - r.append("[") - r.append(("heading_key", "t")) - r.append(":%s]"%self.master.stickycookie_txt) - if self.master.stickyauth_txt: - r.append("[") - r.append(("heading_key", "u")) - r.append(":%s]"%self.master.stickyauth_txt) - if self.master.state.default_body_view.name != "Auto": - r.append("[") - r.append(("heading_key", "M")) - r.append(":%s]"%self.master.state.default_body_view.name) - - opts = [] - if self.master.anticache: - opts.append("anticache") - if self.master.anticomp: - opts.append("anticomp") - if self.master.showhost: - opts.append("showhost") - if not self.master.refresh_server_playback: - opts.append("norefresh") - if self.master.killextra: - opts.append("killextra") - if self.master.server.config.no_upstream_cert: - opts.append("no-upstream-cert") - if self.master.state.follow_focus: - opts.append("following") - if self.master.stream_large_bodies: - opts.append("stream:%s" % utils.pretty_size(self.master.stream_large_bodies.max_size)) - - if opts: - r.append("[%s]"%(":".join(opts))) - - if self.master.server.config.mode in ["reverse", "upstream"]: - dst = self.master.server.config.mode.dst - scheme = "https" if dst[0] else "http" - if dst[1] != dst[0]: - scheme += "2https" if dst[1] else "http" - r.append("[dest:%s]"%utils.unparse_url(scheme, *dst[2:])) - if self.master.scripts: - r.append("[") - r.append(("heading_key", "s")) - r.append("cripts:%s]"%len(self.master.scripts)) - # r.append("[lt:%0.3f]"%self.master.looptime) - - if self.master.stream: - r.append("[W:%s]"%self.master.stream_path) - - return r - - def redraw(self): - if self.ab.expire and time.time() > self.ab.expire: - self.message("") - - fc = self.master.state.flow_count() - if self.master.state.focus is None: - offset = 0 - else: - offset = min(self.master.state.focus + 1, fc) - t = [ - ('heading', ("[%s/%s]"%(offset, fc)).ljust(9)) - ] - - if self.master.server.bound: - host = self.master.server.address.host - if host == "0.0.0.0": - host = "*" - boundaddr = "[%s:%s]"%(host, self.master.server.address.port) - else: - boundaddr = "" - t.extend(self.get_status()) - status = urwid.AttrWrap(urwid.Columns([ - urwid.Text(t), - urwid.Text( - [ - self.helptext, - boundaddr - ], - align="right" - ), - ]), "heading") - self.ib._w = status - - def update(self, text): - self.helptext = text - self.redraw() - self.master.loop.draw_screen() - - 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: - expire = time.time() + float(expire)/1000 - self.ab.message(msg, expire) - self.master.loop.draw_screen() - - class ConsoleState(flow.State): def __init__(self): flow.State.__init__(self) @@ -763,7 +588,7 @@ class ConsoleMaster(flow.FlowMaster): self.help_context, (self.statusbar, self.body, self.header) ) - self.statusbar = StatusBar(self, help.footer) + self.statusbar = statusbar.StatusBar(self, help.footer) self.body = h self.header = None self.loop.widget = self.make_view() @@ -774,7 +599,7 @@ class ConsoleMaster(flow.FlowMaster): flow, (self.statusbar, self.body, self.header) ) - self.statusbar = StatusBar(self, flowdetailview.footer) + self.statusbar = statusbar.StatusBar(self, flowdetailview.footer) self.body = h self.header = None self.loop.widget = self.make_view() @@ -783,7 +608,7 @@ class ConsoleMaster(flow.FlowMaster): self.body = ge self.header = None self.help_context = ge.make_help() - self.statusbar = StatusBar(self, grideditor.footer) + self.statusbar = statusbar.StatusBar(self, grideditor.footer) self.loop.widget = self.make_view() def view_flowlist(self): @@ -796,7 +621,7 @@ class ConsoleMaster(flow.FlowMaster): self.body = flowlist.BodyPile(self) else: self.body = flowlist.FlowListBox(self) - self.statusbar = StatusBar(self, flowlist.footer) + self.statusbar = statusbar.StatusBar(self, flowlist.footer) self.header = None self.state.view_mode = common.VIEW_LIST @@ -806,7 +631,7 @@ class ConsoleMaster(flow.FlowMaster): def view_flow(self, flow): self.body = flowview.FlowView(self, self.state, flow) self.header = flowview.FlowViewHeader(self, flow) - self.statusbar = StatusBar(self, flowview.footer) + self.statusbar = statusbar.StatusBar(self, flowview.footer) self.state.set_focus_flow(flow) self.state.view_mode = common.VIEW_FLOW self.loop.widget = self.make_view() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py new file mode 100644 index 00000000..4fb717cd --- /dev/null +++ b/libmproxy/console/statusbar.py @@ -0,0 +1,180 @@ + +import time + +import urwid + +from . import pathedit +from .. import utils + + +class ActionBar(urwid.WidgetWrap): + def __init__(self): + self.message("") + + def selectable(self): + return True + + def path_prompt(self, prompt, text): + self.expire = None + self._w = pathedit.PathEdit(prompt, text) + + def prompt(self, prompt, text = ""): + self.expire = None + self._w = urwid.Edit(prompt, text or "") + + def message(self, message, expire=None): + self.expire = expire + self._w = urwid.Text(message) + + +class StatusBar(urwid.WidgetWrap): + def __init__(self, master, helptext): + self.master, self.helptext = master, helptext + self.ab = ActionBar() + self.ib = urwid.WidgetWrap(urwid.Text("")) + self._w = urwid.Pile([self.ib, self.ab]) + + def get_status(self): + r = [] + + if self.master.setheaders.count(): + r.append("[") + r.append(("heading_key", "H")) + r.append("eaders]") + if self.master.replacehooks.count(): + r.append("[") + r.append(("heading_key", "R")) + r.append("eplacing]") + if self.master.client_playback: + r.append("[") + r.append(("heading_key", "cplayback")) + r.append(":%s to go]"%self.master.client_playback.count()) + if self.master.server_playback: + r.append("[") + r.append(("heading_key", "splayback")) + if self.master.nopop: + r.append(":%s in file]"%self.master.server_playback.count()) + else: + r.append(":%s to go]"%self.master.server_playback.count()) + if self.master.get_ignore_filter(): + r.append("[") + r.append(("heading_key", "I")) + r.append("gnore:%d]" % len(self.master.get_ignore_filter())) + if self.master.get_tcp_filter(): + r.append("[") + r.append(("heading_key", "T")) + r.append("CP:%d]" % len(self.master.get_tcp_filter())) + if self.master.state.intercept_txt: + r.append("[") + r.append(("heading_key", "i")) + r.append(":%s]"%self.master.state.intercept_txt) + if self.master.state.limit_txt: + r.append("[") + r.append(("heading_key", "l")) + r.append(":%s]"%self.master.state.limit_txt) + if self.master.stickycookie_txt: + r.append("[") + r.append(("heading_key", "t")) + r.append(":%s]"%self.master.stickycookie_txt) + if self.master.stickyauth_txt: + r.append("[") + r.append(("heading_key", "u")) + r.append(":%s]"%self.master.stickyauth_txt) + if self.master.state.default_body_view.name != "Auto": + r.append("[") + r.append(("heading_key", "M")) + r.append(":%s]"%self.master.state.default_body_view.name) + + opts = [] + if self.master.anticache: + opts.append("anticache") + if self.master.anticomp: + opts.append("anticomp") + if self.master.showhost: + opts.append("showhost") + if not self.master.refresh_server_playback: + opts.append("norefresh") + if self.master.killextra: + opts.append("killextra") + if self.master.server.config.no_upstream_cert: + opts.append("no-upstream-cert") + if self.master.state.follow_focus: + opts.append("following") + if self.master.stream_large_bodies: + opts.append("stream:%s" % utils.pretty_size(self.master.stream_large_bodies.max_size)) + + if opts: + r.append("[%s]"%(":".join(opts))) + + if self.master.server.config.mode in ["reverse", "upstream"]: + dst = self.master.server.config.mode.dst + scheme = "https" if dst[0] else "http" + if dst[1] != dst[0]: + scheme += "2https" if dst[1] else "http" + r.append("[dest:%s]"%utils.unparse_url(scheme, *dst[2:])) + if self.master.scripts: + r.append("[") + r.append(("heading_key", "s")) + r.append("cripts:%s]"%len(self.master.scripts)) + # r.append("[lt:%0.3f]"%self.master.looptime) + + if self.master.stream: + r.append("[W:%s]"%self.master.stream_path) + + return r + + def redraw(self): + if self.ab.expire and time.time() > self.ab.expire: + self.message("") + + fc = self.master.state.flow_count() + if self.master.state.focus is None: + offset = 0 + else: + offset = min(self.master.state.focus + 1, fc) + t = [ + ('heading', ("[%s/%s]"%(offset, fc)).ljust(9)) + ] + + if self.master.server.bound: + host = self.master.server.address.host + if host == "0.0.0.0": + host = "*" + boundaddr = "[%s:%s]"%(host, self.master.server.address.port) + else: + boundaddr = "" + t.extend(self.get_status()) + status = urwid.AttrWrap(urwid.Columns([ + urwid.Text(t), + urwid.Text( + [ + self.helptext, + boundaddr + ], + align="right" + ), + ]), "heading") + self.ib._w = status + + def update(self, text): + self.helptext = text + self.redraw() + self.master.loop.draw_screen() + + 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: + expire = time.time() + float(expire)/1000 + self.ab.message(msg, expire) + self.master.loop.draw_screen() -- cgit v1.2.3 From c182133d645a07b7dee4504ecf6f99cc3f72f93a Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 13:26:08 +1300 Subject: console: pull primary window frame management out into window.py --- libmproxy/console/__init__.py | 152 ++--------------------------------------- libmproxy/console/statusbar.py | 3 + libmproxy/console/window.py | 150 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 148 deletions(-) create mode 100644 libmproxy/console/window.py (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 5f564a20..426dda58 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -15,7 +15,7 @@ import urwid import weakref from .. import controller, flow, script -from . import flowlist, flowview, help, common +from . import flowlist, flowview, help, common, window from . import grideditor, palettes, contentview, flowdetailview, statusbar EVENTLOG_SIZE = 500 @@ -146,7 +146,6 @@ class ConsoleMaster(flow.FlowMaster): def __init__(self, server, options): flow.FlowMaster.__init__(self, server, ConsoleState()) - self.looptime = 0 self.stream_path = None self.options = options @@ -363,149 +362,6 @@ class ConsoleMaster(flow.FlowMaster): def set_palette(self, name): self.palette = palettes.palettes[name] - def input_filter(self, keys, raw): - 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: - if common.is_keypress(k): - self.view.keypress(self.loop.screen_size, k) - else: - k = self.view.keypress(self.loop.screen_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 == "H": - self.view_grideditor( - grideditor.SetHeadersEditor( - self, - self.setheaders.get_specs(), - self.setheaders.set - ) - ) - elif k == "I": - self.view_grideditor( - grideditor.HostPatternEditor( - self, - [[x] for x in self.get_ignore_filter()], - self.edit_ignore_filter - ) - ) - elif k == "T": - self.view_grideditor( - grideditor.HostPatternEditor( - self, - [[x] for x in self.get_tcp_filter()], - self.edit_tcp_filter - ) - ) - elif k == "i": - self.prompt( - "Intercept filter: ", - self.state.intercept_txt, - self.set_intercept - ) - elif k == "Q": - raise urwid.ExitMainLoop - elif k == "q": - self.prompt_onekey( - "Quit", - ( - ("yes", "y"), - ("no", "n"), - ), - self.quit, - ) - elif k == "M": - self.prompt_onekey( - "Global default display mode", - contentview.view_prompts, - self.change_default_display_mode - ) - elif k == "R": - self.view_grideditor( - grideditor.ReplaceEditor( - self, - self.replacehooks.get_specs(), - self.replacehooks.set - ) - ) - elif k == "s": - self.view_grideditor( - grideditor.ScriptEditor( - self, - [[i.command] for i in self.scripts], - self.edit_scripts - ) - ) - elif k == "S": - if not self.server_playback: - self.path_prompt( - "Server replay path: ", - 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 == "o": - self.prompt_onekey( - "Options", - ( - ("anticache", "a"), - ("anticomp", "c"), - ("showhost", "h"), - ("killextra", "k"), - ("norefresh", "n"), - ("no-upstream-certs", "u"), - ), - 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.statusbar.redraw() - def ticker(self, *userdata): changed = self.tick(self.masterq, timeout=0) if changed: @@ -528,7 +384,6 @@ class ConsoleMaster(flow.FlowMaster): self.loop = urwid.MainLoop( self.view, screen = self.ui, - input_filter = self.input_filter ) self.view_flowlist() self.statusbar.redraw() @@ -574,12 +429,13 @@ class ConsoleMaster(flow.FlowMaster): self.shutdown() def make_view(self): - self.view = urwid.Frame( + self.view = window.Window( + self, self.body, header = self.header, footer = self.statusbar ) - self.view.set_focus("body") + self.statusbar.redraw() return self.view def view_help(self): diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index 4fb717cd..a38615b4 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -34,6 +34,9 @@ class StatusBar(urwid.WidgetWrap): self.ib = urwid.WidgetWrap(urwid.Text("")) self._w = urwid.Pile([self.ib, self.ab]) + def keypress(self, *args, **kwargs): + return self.ab.keypress(*args, **kwargs) + def get_status(self): r = [] diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py new file mode 100644 index 00000000..8019adce --- /dev/null +++ b/libmproxy/console/window.py @@ -0,0 +1,150 @@ +import urwid + +class Window(urwid.Frame): + def __init__(self, master, body, header, footer): + urwid.Frame.__init__(self, body, header=header, footer=footer) + self.master = master + + def keypress(self, size, k): + if self.master.prompting: + if k == "esc": + self.master.prompt_cancel() + elif self.master.onekey: + if k == "enter": + self.master.prompt_cancel() + elif k in self.master.onekey: + self.master.prompt_execute(k) + elif k == "enter": + self.master.prompt_execute() + else: + if common.is_keypress(k): + urwid.Frame.keypress(self, self.master.loop.screen_size, k) + else: + return k + else: + k = urwid.Frame.keypress(self, self.master.loop.screen_size, k) + if k == "?": + self.master.view_help() + elif k == "c": + if not self.master.client_playback: + self.master.path_prompt( + "Client replay: ", + self.master.state.last_saveload, + self.master.client_playback_path + ) + else: + self.master.prompt_onekey( + "Stop current client replay?", + ( + ("yes", "y"), + ("no", "n"), + ), + self.master.stop_client_playback_prompt, + ) + elif k == "H": + self.master.view_grideditor( + grideditor.SetHeadersEditor( + self.master, + self.master.setheaders.get_specs(), + self.master.setheaders.set + ) + ) + elif k == "I": + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + [[x] for x in self.master.get_ignore_filter()], + self.master.edit_ignore_filter + ) + ) + elif k == "T": + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + [[x] for x in self.master.get_tcp_filter()], + self.master.edit_tcp_filter + ) + ) + elif k == "i": + self.master.prompt( + "Intercept filter: ", + self.master.state.intercept_txt, + self.master.set_intercept + ) + elif k == "Q": + raise urwid.ExitMainLoop + elif k == "q": + self.master.prompt_onekey( + "Quit", + ( + ("yes", "y"), + ("no", "n"), + ), + self.master.quit, + ) + elif k == "M": + self.master.prompt_onekey( + "Global default display mode", + contentview.view_prompts, + self.master.change_default_display_mode + ) + elif k == "R": + self.master.view_grideditor( + grideditor.ReplaceEditor( + self.master, + self.master.replacehooks.get_specs(), + self.master.replacehooks.set + ) + ) + elif k == "s": + self.master.view_grideditor( + grideditor.ScriptEditor( + self.master, + [[i.command] for i in self.master.scripts], + self.master.edit_scripts + ) + ) + elif k == "S": + if not self.master.server_playback: + self.master.path_prompt( + "Server replay path: ", + self.master.state.last_saveload, + self.master.server_playback_path + ) + else: + self.master.prompt_onekey( + "Stop current server replay?", + ( + ("yes", "y"), + ("no", "n"), + ), + self.master.stop_server_playback_prompt, + ) + elif k == "o": + self.master.prompt_onekey( + "Options", + ( + ("anticache", "a"), + ("anticomp", "c"), + ("showhost", "h"), + ("killextra", "k"), + ("norefresh", "n"), + ("no-upstream-certs", "u"), + ), + self.master._change_options + ) + elif k == "t": + self.master.prompt( + "Sticky cookie filter: ", + self.master.stickycookie_txt, + self.master.set_stickycookie + ) + elif k == "u": + self.master.prompt( + "Sticky auth filter: ", + self.master.stickyauth_txt, + self.master.set_stickyauth + ) + else: + return k + self.footer.redraw() -- cgit v1.2.3 From b475c8d6eacd0d6a100cf6aaddc9c9915fdfb149 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 20 Mar 2015 15:22:05 +1300 Subject: Add window.py import missed in refactoring --- libmproxy/console/window.py | 1 + 1 file changed, 1 insertion(+) (limited to 'libmproxy') diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index 8019adce..69f35183 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -1,4 +1,5 @@ import urwid +from . import common class Window(urwid.Frame): def __init__(self, master, body, header, footer): -- cgit v1.2.3 From 8725d50d03cf21b37a78c1d2fa03ade055c8a821 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 21 Mar 2015 11:19:20 +1300 Subject: Add blinker dependency, start using it to refactor console app Blinker lets us set up a central pub/sub mechanism to disentangle our object structure. --- libmproxy/console/__init__.py | 22 ++++++++++++---------- libmproxy/console/common.py | 11 ++++++----- libmproxy/console/flowlist.py | 10 +++++----- libmproxy/console/flowview.py | 24 ++++++++++++------------ libmproxy/console/grideditor.py | 8 ++++---- libmproxy/console/signals.py | 4 ++++ libmproxy/console/statusbar.py | 19 +++++-------------- libmproxy/console/window.py | 2 +- 8 files changed, 49 insertions(+), 51 deletions(-) create mode 100644 libmproxy/console/signals.py (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 426dda58..b5c59ecf 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -15,7 +15,7 @@ import urwid import weakref from .. import controller, flow, script -from . import flowlist, flowview, help, common, window +from . import flowlist, flowview, help, common, window, signals from . import grideditor, palettes, contentview, flowdetailview, statusbar EVENTLOG_SIZE = 500 @@ -238,7 +238,9 @@ class ConsoleMaster(flow.FlowMaster): try: s = script.Script(command, self) except script.ScriptError, v: - self.statusbar.message("Error loading script.") + signals.status_message.send( + message = "Error loading script." + ) self.add_event("Error loading script:\n%s"%v.args[0], "error") return @@ -257,7 +259,7 @@ class ConsoleMaster(flow.FlowMaster): return ret = self.load_script(command) if ret: - self.statusbar.message(ret) + signals.status_message.send(message=ret) self.state.last_script = command def toggle_eventlog(self): @@ -279,7 +281,7 @@ class ConsoleMaster(flow.FlowMaster): print >> sys.stderr, e.strerror sys.exit(1) else: - self.statusbar.message(e.strerror) + signals.status_message.send(message=e.strerror) return None def client_playback_path(self, path): @@ -314,7 +316,7 @@ class ConsoleMaster(flow.FlowMaster): try: subprocess.call(cmd) except: - self.statusbar.message("Can't start editor: %s" % " ".join(c)) + signals.status_message.send(message="Can't start editor: %s" % " ".join(c)) else: data = open(name, "rb").read() self.ui.start() @@ -353,8 +355,8 @@ class ConsoleMaster(flow.FlowMaster): try: subprocess.call(cmd, shell=shell) except: - self.statusbar.message( - "Can't start external viewer: %s" % " ".join(c) + signals.status_message.send( + message="Can't start external viewer: %s" % " ".join(c) ) self.ui.start() os.unlink(name) @@ -505,7 +507,7 @@ class ConsoleMaster(flow.FlowMaster): fw.add(i) f.close() except IOError, v: - self.statusbar.message(v.strerror) + signals.status_message.send(message=v.strerror) def save_one_flow(self, path, flow): return self._write_flows(path, [flow]) @@ -565,7 +567,7 @@ class ConsoleMaster(flow.FlowMaster): self.prompting = False self.onekey = False self.view.set_focus("body") - self.statusbar.message("") + signals.status_message.send(message="") def prompt_execute(self, txt=None): if not txt: @@ -574,7 +576,7 @@ class ConsoleMaster(flow.FlowMaster): self.prompt_done() msg = p(txt, *args) if msg: - self.statusbar.message(msg, 1000) + signals.status_message.send(message=msg, expire=1000) def prompt_cancel(self): self.prompt_done() diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 90204d79..9731b682 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -6,6 +6,7 @@ import os from .. import utils from ..protocol.http import CONTENT_MISSING, decoded +from . import signals try: import pyperclip @@ -198,7 +199,7 @@ def save_data(path, data, master, state): with file(path, "wb") as f: f.write(data) except IOError, v: - master.statusbar.message(v.strerror) + signals.status_message.send(message=v.strerror) def ask_save_path(prompt, data, master, state): @@ -248,11 +249,11 @@ def copy_flow(part, scope, flow, master, state): if not data: if scope == "q": - master.statusbar.message("No request content to copy.") + signals.status_message.send(message="No request content to copy.") elif scope == "s": - master.statusbar.message("No response content to copy.") + signals.status_message.send(message="No response content to copy.") else: - master.statusbar.message("No contents to copy.") + signals.status_message.send(message="No contents to copy.") return try: @@ -336,7 +337,7 @@ def ask_save_body(part, master, state, flow): state ) else: - master.statusbar.message("No content to save.") + signals.status_message.send(message="No content to save.") class FlowCache: diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index 5d8ad942..c8ecf15c 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import urwid from netlib import http -from . import common +from . import common, signals def _mkhelp(): @@ -171,7 +171,7 @@ class ConnectionItem(urwid.WidgetWrap): elif key == "r": r = self.master.replay_request(self.flow) if r: - self.master.statusbar.message(r) + signals.status_message.send(message=r) self.master.sync_list_view() elif key == "S": if not self.master.server_playback: @@ -195,11 +195,11 @@ class ConnectionItem(urwid.WidgetWrap): ) elif key == "V": if not self.flow.modified(): - self.master.statusbar.message("Flow not modified.") + signals.status_message.send(message="Flow not modified.") return self.state.revert(self.flow) self.master.sync_list_view() - self.master.statusbar.message("Reverted.") + signals.status_message.send(message="Reverted.") elif key == "w": self.master.prompt_onekey( "Save", @@ -285,7 +285,7 @@ class FlowListBox(urwid.ListBox): def new_request(self, url, method): parts = http.parse_url(str(url)) if not parts: - self.master.statusbar.message("Invalid Url") + signals.status_message.send(message="Invalid Url") return scheme, host, port, path = parts f = self.master.create_request(method, scheme, host, port, path) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 89e75aad..b22bbb37 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import os, sys, copy import urwid -from . import common, grideditor, contentview +from . import common, grideditor, contentview, signals from .. import utils, flow, controller from ..protocol.http import HTTPRequest, HTTPResponse, CONTENT_MISSING, decoded @@ -282,10 +282,10 @@ class FlowView(urwid.WidgetWrap): if last_search_string: message = self.search(last_search_string, backwards) if message: - self.master.statusbar.message(message) + signals.status_message.send(message=message) else: message = "no previous searches have been made" - self.master.statusbar.message(message) + signals.status_message.send(message=message) return message @@ -606,7 +606,7 @@ class FlowView(urwid.WidgetWrap): else: new_flow, new_idx = self.state.get_prev(idx) if new_flow is None: - self.master.statusbar.message("No more flows!") + signals.status_message.send(message="No more flows!") return self.master.view_flow(new_flow) @@ -681,7 +681,7 @@ class FlowView(urwid.WidgetWrap): elif key == "D": f = self.master.duplicate_flow(self.flow) self.master.view_flow(f) - self.master.statusbar.message("Duplicated.") + signals.status_message.send(message="Duplicated.") elif key == "e": if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: self.master.prompt_onekey( @@ -710,14 +710,14 @@ class FlowView(urwid.WidgetWrap): ) key = None elif key == "f": - self.master.statusbar.message("Loading all body data...") + signals.status_message.send(message="Loading all body data...") self.state.add_flow_setting( self.flow, (self.state.view_flow_mode, "fullcontents"), True ) self.master.refresh_flow(self.flow) - self.master.statusbar.message("") + signals.status_message.send(message="") elif key == "g": if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: scope = "q" @@ -738,15 +738,15 @@ class FlowView(urwid.WidgetWrap): elif key == "r": r = self.master.replay_request(self.flow) if r: - self.master.statusbar.message(r) + signals.status_message.send(message=r) self.master.refresh_flow(self.flow) elif key == "V": if not self.flow.modified(): - self.master.statusbar.message("Flow not modified.") + signals.status_message.send(message="Flow not modified.") return self.state.revert(self.flow) self.master.refresh_flow(self.flow) - self.master.statusbar.message("Reverted.") + signals.status_message.send(message="Reverted.") elif key == "W": self.master.path_prompt( "Save this flow: ", @@ -761,7 +761,7 @@ class FlowView(urwid.WidgetWrap): if os.environ.has_key("EDITOR") or os.environ.has_key("PAGER"): self.master.spawn_external_viewer(conn.content, t) else: - self.master.statusbar.message("Error! Set $EDITOR or $PAGER.") + signals.status_message.send(message="Error! Set $EDITOR or $PAGER.") elif key == "|": self.master.path_prompt( "Send flow to script: ", self.state.last_script, @@ -785,7 +785,7 @@ class FlowView(urwid.WidgetWrap): e = conn.headers.get_first("content-encoding", "identity") if e != "identity": if not conn.decode(): - self.master.statusbar.message("Could not decode - invalid data?") + signals.status_message.send(message="Could not decode - invalid data?") else: self.master.prompt_onekey( "Select encoding: ", diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index fe3df509..2d2754b1 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -5,7 +5,7 @@ import re import os import urwid -from . import common +from . import common, signals from .. import utils, filt, script from netlib import http_uastrings @@ -125,14 +125,14 @@ class GridWalker(urwid.ListWalker): try: val = val.decode("string-escape") except ValueError: - self.editor.master.statusbar.message( - "Invalid Python-style string encoding.", 1000 + signals.status_message.send( + self, message = "Invalid Python-style string encoding.", expure = 1000 ) return errors = self.lst[self.focus][1] emsg = self.editor.is_error(self.focus_col, val) if emsg: - self.editor.master.statusbar.message(emsg, 1000) + signals.status_message.send(message = emsg, expire = 1000) errors.add(self.focus_col) else: errors.discard(self.focus_col) diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py new file mode 100644 index 00000000..a844ef8f --- /dev/null +++ b/libmproxy/console/signals.py @@ -0,0 +1,4 @@ + +import blinker + +status_message = blinker.Signal() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index a38615b4..7ad78f03 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -3,26 +3,26 @@ import time import urwid -from . import pathedit +from . import pathedit, signals from .. import utils + class ActionBar(urwid.WidgetWrap): def __init__(self): - self.message("") + urwid.WidgetWrap.__init__(self, urwid.Text("")) + signals.status_message.connect(self.message) def selectable(self): return True def path_prompt(self, prompt, text): - self.expire = None self._w = pathedit.PathEdit(prompt, text) def prompt(self, prompt, text = ""): - self.expire = None self._w = urwid.Edit(prompt, text or "") - def message(self, message, expire=None): + def message(self, sender, message, expire=None): self.expire = expire self._w = urwid.Text(message) @@ -127,9 +127,6 @@ class StatusBar(urwid.WidgetWrap): return r def redraw(self): - if self.ab.expire and time.time() > self.ab.expire: - self.message("") - fc = self.master.state.flow_count() if self.master.state.focus is None: offset = 0 @@ -175,9 +172,3 @@ class StatusBar(urwid.WidgetWrap): def prompt(self, prompt, text = ""): self.ab.prompt(prompt, text) - - def message(self, msg, expire=None): - if expire: - expire = time.time() + float(expire)/1000 - self.ab.message(msg, expire) - self.master.loop.draw_screen() diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index 69f35183..44a5a316 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -1,5 +1,5 @@ import urwid -from . import common +from . import common, grideditor class Window(urwid.Frame): def __init__(self, master, body, header, footer): -- cgit v1.2.3 From 381a56306777900153939b1b46f20e63322944c2 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 21 Mar 2015 12:37:00 +1300 Subject: Status bar message expiry based on signals and Urwid main loop --- libmproxy/console/__init__.py | 8 +++++++- libmproxy/console/grideditor.py | 2 +- libmproxy/console/signals.py | 1 + libmproxy/console/statusbar.py | 21 ++++++++++++++------- 4 files changed, 23 insertions(+), 9 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index b5c59ecf..aae7a9c4 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -212,6 +212,12 @@ class ConsoleMaster(flow.FlowMaster): if options.app: self.start_app(self.options.app_host, self.options.app_port) + signals.call_in.connect(self.sig_call_in) + + def sig_call_in(self, sender, seconds, callback, args=()): + def cb(*_): + return callback(*args) + self.loop.set_alarm_in(seconds, cb) def start_stream_to_path(self, path, mode="wb"): path = os.path.expanduser(path) @@ -576,7 +582,7 @@ class ConsoleMaster(flow.FlowMaster): self.prompt_done() msg = p(txt, *args) if msg: - signals.status_message.send(message=msg, expire=1000) + signals.status_message.send(message=msg, expire=1) def prompt_cancel(self): self.prompt_done() diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index 2d2754b1..0b563c52 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -132,7 +132,7 @@ class GridWalker(urwid.ListWalker): errors = self.lst[self.focus][1] emsg = self.editor.is_error(self.focus_col, val) if emsg: - signals.status_message.send(message = emsg, expire = 1000) + signals.status_message.send(message = emsg, expire = 1) errors.add(self.focus_col) else: errors.discard(self.focus_col) diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py index a844ef8f..7b0ec937 100644 --- a/libmproxy/console/signals.py +++ b/libmproxy/console/signals.py @@ -2,3 +2,4 @@ import blinker status_message = blinker.Signal() +call_in = blinker.Signal() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index 7ad78f03..a29767e4 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -1,4 +1,3 @@ - import time import urwid @@ -7,11 +6,14 @@ from . import pathedit, signals from .. import utils - class ActionBar(urwid.WidgetWrap): def __init__(self): - urwid.WidgetWrap.__init__(self, urwid.Text("")) - signals.status_message.connect(self.message) + urwid.WidgetWrap.__init__(self, None) + self.clear() + signals.status_message.connect(self.sig_message) + + def clear(self): + self._w = urwid.Text("") def selectable(self): return True @@ -22,9 +24,14 @@ class ActionBar(urwid.WidgetWrap): def prompt(self, prompt, text = ""): self._w = urwid.Edit(prompt, text or "") - def message(self, sender, message, expire=None): - self.expire = expire - self._w = urwid.Text(message) + def sig_message(self, sender, message, expire=None): + w = urwid.Text(message) + self._w = w + if expire: + def cb(*args): + if w == self._w: + self.clear() + signals.call_in.send(seconds=expire, callback=cb) class StatusBar(urwid.WidgetWrap): -- cgit v1.2.3 From ac5d74d42c0824b5789cc030bf39a447951e4804 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 21 Mar 2015 21:55:02 +0100 Subject: web: fix bugs --- libmproxy/web/static/app.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index dae10a34..ef53c08f 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -421,6 +421,7 @@ module.exports = { ConnectionActions: ConnectionActions, FlowActions: FlowActions, StoreCmds: StoreCmds, + SettingsActions: SettingsActions, Query: Query }; @@ -622,6 +623,7 @@ var common = require("./common.js"); var Query = require("../actions.js").Query; var VirtualScrollMixin = require("./virtualscroll.js"); var views = require("../store/view.js"); +var _ = require("lodash"); var LogMessage = React.createClass({displayName: "LogMessage", render: function () { @@ -775,7 +777,7 @@ var EventLog = React.createClass({displayName: "EventLog", module.exports = EventLog; -},{"../actions.js":2,"../store/view.js":19,"./common.js":4,"./virtualscroll.js":13,"react":"react"}],6:[function(require,module,exports){ +},{"../actions.js":2,"../store/view.js":19,"./common.js":4,"./virtualscroll.js":13,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1774,7 +1776,7 @@ var MainMenu = React.createClass({displayName: "MainMenu", this.setQuery(d); }, onInterceptChange: function (val) { - SettingsActions.update({intercept: val}); + actions.SettingsActions.update({intercept: val}); }, render: function () { var filter = this.getQuery()[Query.FILTER] || ""; @@ -2196,6 +2198,8 @@ var MainView = React.createClass({displayName: "MainView", actions.FlowActions.revert(flow); } break; + case toputils.Key.SHIFT: + break; default: console.debug("keydown", e.keyCode); return; @@ -4514,8 +4518,6 @@ var default_filt = function (elem) { function StoreView(store, filt, sortfun) { EventEmitter.call(this); - filt = filt || default_filt; - sortfun = sortfun || default_sort; this.store = store; @@ -4539,10 +4541,10 @@ _.extend(StoreView.prototype, EventEmitter.prototype, { this.store.removeListener("recalculate", this.recalculate); }, recalculate: function (filt, sortfun) { - filt = filt || default_filt; - sortfun = sortfun || default_sort; + filt = filt || this.filt || default_filt; + sortfun = sortfun || this.sortfun || default_sort; filt = filt.bind(this); - sortfun = sortfun.bind(this) + sortfun = sortfun.bind(this); this.filt = filt; this.sortfun = sortfun; @@ -4633,6 +4635,7 @@ var Key = { TAB: 9, SPACE: 32, BACKSPACE: 8, + SHIFT: 16 }; // Add A-Z for (var i = 65; i <= 90; i++) { -- cgit v1.2.3 From 02a61ea45dc1ca6d0c88b44adf83f68b791130e7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 21 Mar 2015 22:49:51 +0100 Subject: structure components --- libmproxy/protocol/http.py | 2 + libmproxy/web/static/app.js | 1031 ++++++++++++++++++++++--------------------- 2 files changed, 530 insertions(+), 503 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 49310ec3..00086c21 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -119,6 +119,8 @@ class HTTPMessage(stateobject.StateObject): if short: if self.content: ret["contentLength"] = len(self.content) + elif self.content == CONTENT_MISSING: + ret["contentLength"] = None else: ret["contentLength"] = 0 return ret diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index ef53c08f..04d6f282 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -443,7 +443,7 @@ $(function () { -},{"./components/proxyapp.js":12,"./connection":14,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ +},{"./components/proxyapp.js":15,"./connection":17,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -777,156 +777,399 @@ var EventLog = React.createClass({displayName: "EventLog", module.exports = EventLog; -},{"../actions.js":2,"../store/view.js":19,"./common.js":4,"./virtualscroll.js":13,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ +},{"../actions.js":2,"../store/view.js":22,"./common.js":4,"./virtualscroll.js":16,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ var React = require("react"); -var _ = require("lodash"); - -var common = require("./common.js"); -var actions = require("../actions.js"); -var flowutils = require("../flow/utils.js"); -var toputils = require("../utils.js"); +var RequestUtils = require("../flow/utils.js").RequestUtils; +var ResponseUtils = require("../flow/utils.js").ResponseUtils; +var utils = require("../utils.js"); -var NavAction = React.createClass({displayName: "NavAction", - onClick: function (e) { - e.preventDefault(); - this.props.onClick(); +var TLSColumn = React.createClass({displayName: "TLSColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-tls " + (this.props.className || "") })); + } + }), + sortKeyFun: function(flow){ + return flow.request.scheme; + } }, render: function () { - return ( - React.createElement("a", {title: this.props.title, - href: "#", - className: "nav-action", - onClick: this.onClick}, - React.createElement("i", {className: "fa fa-fw " + this.props.icon}) - ) - ); + var flow = this.props.flow; + var ssl = (flow.request.scheme == "https"); + var classes; + if (ssl) { + classes = "col-tls col-tls-https"; + } else { + classes = "col-tls col-tls-http"; + } + return React.createElement("td", {className: classes}); } }); -var FlowDetailNav = React.createClass({displayName: "FlowDetailNav", + +var IconColumn = React.createClass({displayName: "IconColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-icon " + (this.props.className || "") })); + } + }) + }, render: function () { var flow = this.props.flow; - var tabs = this.props.tabs.map(function (e) { - var str = e.charAt(0).toUpperCase() + e.slice(1); - var className = this.props.active === e ? "active" : ""; - var onClick = function (event) { - this.props.selectTab(e); - event.preventDefault(); - }.bind(this); - return React.createElement("a", {key: e, - href: "#", - className: className, - onClick: onClick}, str); - }.bind(this)); + var icon; + if (flow.response) { + var contentType = ResponseUtils.getContentType(flow.response); - var acceptButton = null; - if(flow.intercepted){ - acceptButton = React.createElement(NavAction, {title: "[a]ccept intercepted flow", icon: "fa-play", onClick: actions.FlowActions.accept.bind(null, flow)}); + //TODO: We should assign a type to the flow somewhere else. + if (flow.response.code == 304) { + icon = "resource-icon-not-modified"; + } else if (300 <= flow.response.code && flow.response.code < 400) { + icon = "resource-icon-redirect"; + } else if (contentType && contentType.indexOf("image") >= 0) { + icon = "resource-icon-image"; + } else if (contentType && contentType.indexOf("javascript") >= 0) { + icon = "resource-icon-js"; + } else if (contentType && contentType.indexOf("css") >= 0) { + icon = "resource-icon-css"; + } else if (contentType && contentType.indexOf("html") >= 0) { + icon = "resource-icon-document"; + } } - var revertButton = null; - if(flow.modified){ - revertButton = React.createElement(NavAction, {title: "revert changes to flow [V]", icon: "fa-history", onClick: actions.FlowActions.revert.bind(null, flow)}); + if (!icon) { + icon = "resource-icon-plain"; } - return ( - React.createElement("nav", {ref: "head", className: "nav-tabs nav-tabs-sm"}, - tabs, - React.createElement(NavAction, {title: "[d]elete flow", icon: "fa-trash", onClick: actions.FlowActions.delete.bind(null, flow)}), - React.createElement(NavAction, {title: "[D]uplicate flow", icon: "fa-copy", onClick: actions.FlowActions.duplicate.bind(null, flow)}), - React.createElement(NavAction, {disabled: true, title: "[r]eplay flow", icon: "fa-repeat", onClick: actions.FlowActions.replay.bind(null, flow)}), - acceptButton, - revertButton - ) + + icon += " resource-icon"; + return React.createElement("td", {className: "col-icon"}, + React.createElement("div", {className: icon}) ); } }); -var Headers = React.createClass({displayName: "Headers", +var PathColumn = React.createClass({displayName: "PathColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-path " + (this.props.className || "") }), "Path"); + } + }), + sortKeyFun: function(flow){ + return RequestUtils.pretty_url(flow.request); + } + }, render: function () { - var rows = this.props.message.headers.map(function (header, i) { - return ( - React.createElement("tr", {key: i}, - React.createElement("td", {className: "header-name"}, header[0] + ":"), - React.createElement("td", {className: "header-value"}, header[1]) - ) - ); - }); - return ( - React.createElement("table", {className: "header-table"}, - React.createElement("tbody", null, - rows - ) - ) + var flow = this.props.flow; + return React.createElement("td", {className: "col-path"}, + flow.request.is_replay ? React.createElement("i", {className: "fa fa-fw fa-repeat pull-right"}) : null, + flow.intercepted ? React.createElement("i", {className: "fa fa-fw fa-pause pull-right"}) : null, + RequestUtils.pretty_url(flow.request) ); } }); -var FlowDetailRequest = React.createClass({displayName: "FlowDetailRequest", + +var MethodColumn = React.createClass({displayName: "MethodColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-method " + (this.props.className || "") }), "Method"); + } + }), + sortKeyFun: function(flow){ + return flow.request.method; + } + }, render: function () { var flow = this.props.flow; - var first_line = [ - flow.request.method, - flowutils.RequestUtils.pretty_url(flow.request), - "HTTP/" + flow.request.httpversion.join(".") - ].join(" "); - var content = null; - if (flow.request.contentLength > 0) { - content = "Request Content Size: " + toputils.formatSize(flow.request.contentLength); + return React.createElement("td", {className: "col-method"}, flow.request.method); + } +}); + + +var StatusColumn = React.createClass({displayName: "StatusColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-status " + (this.props.className || "") }), "Status"); + } + }), + sortKeyFun: function(flow){ + return flow.response ? flow.response.code : undefined; + } + }, + render: function () { + var flow = this.props.flow; + var status; + if (flow.response) { + status = flow.response.code; } else { - content = React.createElement("div", {className: "alert alert-info"}, "No Content"); + status = null; } + return React.createElement("td", {className: "col-status"}, status); + } +}); - //TODO: Styling - return ( - React.createElement("section", null, - React.createElement("div", {className: "first-line"}, first_line ), - React.createElement(Headers, {message: flow.request}), - React.createElement("hr", null), - content - ) - ); +var SizeColumn = React.createClass({displayName: "SizeColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-size " + (this.props.className || "") }), "Size"); + } + }), + sortKeyFun: function(flow){ + var total = flow.request.contentLength; + if (flow.response) { + total += flow.response.contentLength || 0; + } + return total; + } + }, + render: function () { + var flow = this.props.flow; + + var total = flow.request.contentLength; + if (flow.response) { + total += flow.response.contentLength || 0; + } + var size = utils.formatSize(total); + return React.createElement("td", {className: "col-size"}, size); } }); -var FlowDetailResponse = React.createClass({displayName: "FlowDetailResponse", + +var TimeColumn = React.createClass({displayName: "TimeColumn", + statics: { + Title: React.createClass({displayName: "Title", + render: function(){ + return React.createElement("th", React.__spread({}, this.props, {className: "col-time " + (this.props.className || "") }), "Time"); + } + }), + sortKeyFun: function(flow){ + if(flow.response) { + return flow.response.timestamp_end - flow.request.timestamp_start; + } + } + }, render: function () { var flow = this.props.flow; - var first_line = [ - "HTTP/" + flow.response.httpversion.join("."), - flow.response.code, - flow.response.msg - ].join(" "); - var content = null; - if (flow.response.contentLength > 0) { - content = "Response Content Size: " + toputils.formatSize(flow.response.contentLength); + var time; + if (flow.response) { + time = utils.formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); } else { - content = React.createElement("div", {className: "alert alert-info"}, "No Content"); + time = "..."; } + return React.createElement("td", {className: "col-time"}, time); + } +}); - //TODO: Styling + +var all_columns = [ + TLSColumn, + IconColumn, + PathColumn, + MethodColumn, + StatusColumn, + SizeColumn, + TimeColumn +]; + +module.exports = all_columns; + +},{"../flow/utils.js":20,"../utils.js":23,"react":"react"}],7:[function(require,module,exports){ +var React = require("react"); +var common = require("./common.js"); +var utils = require("../utils.js"); +var _ = require("lodash"); + +var VirtualScrollMixin = require("./virtualscroll.js"); +var flowtable_columns = require("./flowtable-columns.js"); + +var FlowRow = React.createClass({displayName: "FlowRow", + render: function () { + var flow = this.props.flow; + var columns = this.props.columns.map(function (Column) { + return React.createElement(Column, {key: Column.displayName, flow: flow}); + }.bind(this)); + var className = ""; + if (this.props.selected) { + className += " selected"; + } + if (this.props.highlighted) { + className += " highlighted"; + } + if (flow.intercepted) { + className += " intercepted"; + } + if (flow.request) { + className += " has-request"; + } + if (flow.response) { + className += " has-response"; + } return ( - React.createElement("section", null, - React.createElement("div", {className: "first-line"}, first_line ), - React.createElement(Headers, {message: flow.response}), - React.createElement("hr", null), - content - ) + React.createElement("tr", {className: className, onClick: this.props.selectFlow.bind(null, flow)}, + columns + )); + }, + shouldComponentUpdate: function (nextProps) { + return true; + // Further optimization could be done here + // by calling forceUpdate on flow updates, selection changes and column changes. + //return ( + //(this.props.columns.length !== nextProps.columns.length) || + //(this.props.selected !== nextProps.selected) + //); + } +}); + +var FlowTableHead = React.createClass({displayName: "FlowTableHead", + getInitialState: function(){ + return { + sortColumn: undefined, + sortDesc: false + }; + }, + onClick: function(Column){ + var sortDesc = this.state.sortDesc; + var hasSort = Column.sortKeyFun; + if(Column === this.state.sortColumn){ + sortDesc = !sortDesc; + this.setState({ + sortDesc: sortDesc + }); + } else { + this.setState({ + sortColumn: hasSort && Column, + sortDesc: false + }) + } + var sortKeyFun; + if(!sortDesc){ + sortKeyFun = Column.sortKeyFun; + } else { + sortKeyFun = hasSort && function(){ + var k = Column.sortKeyFun.apply(this, arguments); + if(_.isString(k)){ + return utils.reverseString(""+k); + } else { + return -k; + } + } + } + this.props.setSortKeyFun(sortKeyFun); + }, + render: function () { + var columns = this.props.columns.map(function (Column) { + var onClick = this.onClick.bind(this, Column); + var className; + if(this.state.sortColumn === Column) { + if(this.state.sortDesc){ + className = "sort-desc"; + } else { + className = "sort-asc"; + } + } + return React.createElement(Column.Title, { + key: Column.displayName, + onClick: onClick, + className: className}); + }.bind(this)); + return React.createElement("thead", null, + React.createElement("tr", null, columns) + ); + } +}); + + +var ROW_HEIGHT = 32; + +var FlowTable = React.createClass({displayName: "FlowTable", + mixins: [common.StickyHeadMixin, common.AutoScrollMixin, VirtualScrollMixin], + getInitialState: function () { + return { + columns: flowtable_columns + }; + }, + _listen: function(view){ + if(!view){ + return; + } + view.addListener("add", this.onChange); + view.addListener("update", this.onChange); + view.addListener("remove", this.onChange); + view.addListener("recalculate", this.onChange); + }, + componentWillMount: function () { + this._listen(this.props.view); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.view !== this.props.view) { + if (this.props.view) { + this.props.view.removeListener("add"); + this.props.view.removeListener("update"); + this.props.view.removeListener("remove"); + this.props.view.removeListener("recalculate"); + } + this._listen(nextProps.view); + } + }, + getDefaultProps: function () { + return { + rowHeight: ROW_HEIGHT + }; + }, + onScrollFlowTable: function () { + this.adjustHead(); + this.onScroll(); + }, + onChange: function () { + this.forceUpdate(); + }, + scrollIntoView: function (flow) { + this.scrollRowIntoView( + this.props.view.index(flow), + this.refs.body.getDOMNode().offsetTop + ); + }, + renderRow: function (flow) { + var selected = (flow === this.props.selected); + var highlighted = + ( + this.props.view._highlight && + this.props.view._highlight[flow.id] + ); + + return React.createElement(FlowRow, {key: flow.id, + ref: flow.id, + flow: flow, + columns: this.state.columns, + selected: selected, + highlighted: highlighted, + selectFlow: this.props.selectFlow} ); - } -}); - -var FlowDetailError = React.createClass({displayName: "FlowDetailError", + }, render: function () { - var flow = this.props.flow; + //console.log("render flowtable", this.state.start, this.state.stop, this.props.selected); + var flows = this.props.view ? this.props.view.list : []; + + var rows = this.renderRows(flows); + return ( - React.createElement("section", null, - React.createElement("div", {className: "alert alert-warning"}, - flow.error.msg, - React.createElement("div", null, - React.createElement("small", null, toputils.formatTimeStamp(flow.error.timestamp) ) + React.createElement("div", {className: "flow-table", onScroll: this.onScrollFlowTable}, + React.createElement("table", null, + React.createElement(FlowTableHead, {ref: "head", + columns: this.state.columns, + setSortKeyFun: this.props.setSortKeyFun}), + React.createElement("tbody", {ref: "body"}, + this.getPlaceholderTop(flows.length), + rows, + this.getPlaceholderBottom(flows.length) ) ) ) @@ -934,6 +1177,15 @@ var FlowDetailError = React.createClass({displayName: "FlowDetailError", } }); +module.exports = FlowTable; + + +},{"../utils.js":23,"./common.js":4,"./flowtable-columns.js":6,"./virtualscroll.js":16,"lodash":"lodash","react":"react"}],8:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../../utils.js"); + var TimeStamp = React.createClass({displayName: "TimeStamp", render: function () { @@ -942,11 +1194,11 @@ var TimeStamp = React.createClass({displayName: "TimeStamp", return React.createElement("tr", null); } - var ts = toputils.formatTimeStamp(this.props.t); + var ts = utils.formatTimeStamp(this.props.t); var delta; if (this.props.deltaTo) { - delta = toputils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = utils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); delta = React.createElement("span", {className: "text-muted"}, "(" + delta + ")"); } else { delta = null; @@ -1086,7 +1338,7 @@ var Timing = React.createClass({displayName: "Timing", } }); -var FlowDetailConnectionInfo = React.createClass({displayName: "FlowDetailConnectionInfo", +var Details = React.createClass({displayName: "Details", render: function () { var flow = this.props.flow; var client_conn = flow.client_conn; @@ -1109,14 +1361,25 @@ var FlowDetailConnectionInfo = React.createClass({displayName: "FlowDetailConnec } }); +module.exports = Details; + +},{"../../utils.js":23,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var Nav = require("./nav.js"); +var Messages = require("./messages.js"); +var Details = require("./details.js"); + var allTabs = { - request: FlowDetailRequest, - response: FlowDetailResponse, - error: FlowDetailError, - details: FlowDetailConnectionInfo + request: Messages.Request, + response: Messages.Response, + error: Messages.Error, + details: Details }; -var FlowDetail = React.createClass({displayName: "FlowDetail", +var FlowView = React.createClass({displayName: "FlowView", mixins: [common.StickyHeadMixin, common.Navigation, common.State], getTabs: function (flow) { var tabs = []; @@ -1163,7 +1426,7 @@ var FlowDetail = React.createClass({displayName: "FlowDetail", var Tab = allTabs[active]; return ( React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, - React.createElement(FlowDetailNav, {ref: "head", + React.createElement(Nav, {ref: "head", flow: flow, tabs: tabs, active: active, @@ -1174,414 +1437,176 @@ var FlowDetail = React.createClass({displayName: "FlowDetail", } }); -module.exports = { - FlowDetail: FlowDetail -}; +module.exports = FlowView; -},{"../actions.js":2,"../flow/utils.js":17,"../utils.js":20,"./common.js":4,"lodash":"lodash","react":"react"}],7:[function(require,module,exports){ +},{"../common.js":4,"./details.js":8,"./messages.js":10,"./nav.js":11,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ var React = require("react"); -var RequestUtils = require("../flow/utils.js").RequestUtils; -var ResponseUtils = require("../flow/utils.js").ResponseUtils; -var utils = require("../utils.js"); - -var TLSColumn = React.createClass({displayName: "TLSColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-tls " + (this.props.className || "") })); - } - }), - sortKeyFun: function(flow){ - return flow.request.scheme; - } - }, - render: function () { - var flow = this.props.flow; - var ssl = (flow.request.scheme == "https"); - var classes; - if (ssl) { - classes = "col-tls col-tls-https"; - } else { - classes = "col-tls col-tls-http"; - } - return React.createElement("td", {className: classes}); - } -}); - - -var IconColumn = React.createClass({displayName: "IconColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-icon " + (this.props.className || "") })); - } - }) - }, - render: function () { - var flow = this.props.flow; - - var icon; - if (flow.response) { - var contentType = ResponseUtils.getContentType(flow.response); - - //TODO: We should assign a type to the flow somewhere else. - if (flow.response.code == 304) { - icon = "resource-icon-not-modified"; - } else if (300 <= flow.response.code && flow.response.code < 400) { - icon = "resource-icon-redirect"; - } else if (contentType && contentType.indexOf("image") >= 0) { - icon = "resource-icon-image"; - } else if (contentType && contentType.indexOf("javascript") >= 0) { - icon = "resource-icon-js"; - } else if (contentType && contentType.indexOf("css") >= 0) { - icon = "resource-icon-css"; - } else if (contentType && contentType.indexOf("html") >= 0) { - icon = "resource-icon-document"; - } - } - if (!icon) { - icon = "resource-icon-plain"; - } - - icon += " resource-icon"; - return React.createElement("td", {className: "col-icon"}, - React.createElement("div", {className: icon}) - ); - } -}); +var flowutils = require("../../flow/utils.js"); +var utils = require("../../utils.js"); -var PathColumn = React.createClass({displayName: "PathColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-path " + (this.props.className || "") }), "Path"); - } - }), - sortKeyFun: function(flow){ - return RequestUtils.pretty_url(flow.request); - } - }, +var Headers = React.createClass({displayName: "Headers", render: function () { - var flow = this.props.flow; - return React.createElement("td", {className: "col-path"}, - flow.request.is_replay ? React.createElement("i", {className: "fa fa-fw fa-repeat pull-right"}) : null, - flow.intercepted ? React.createElement("i", {className: "fa fa-fw fa-pause pull-right"}) : null, - RequestUtils.pretty_url(flow.request) + var rows = this.props.message.headers.map(function (header, i) { + return ( + React.createElement("tr", {key: i}, + React.createElement("td", {className: "header-name"}, header[0] + ":"), + React.createElement("td", {className: "header-value"}, header[1]) + ) + ); + }); + return ( + React.createElement("table", {className: "header-table"}, + React.createElement("tbody", null, + rows + ) + ) ); } }); - -var MethodColumn = React.createClass({displayName: "MethodColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-method " + (this.props.className || "") }), "Method"); - } - }), - sortKeyFun: function(flow){ - return flow.request.method; - } - }, - render: function () { - var flow = this.props.flow; - return React.createElement("td", {className: "col-method"}, flow.request.method); - } -}); - - -var StatusColumn = React.createClass({displayName: "StatusColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-status " + (this.props.className || "") }), "Status"); - } - }), - sortKeyFun: function(flow){ - return flow.response ? flow.response.code : undefined; - } - }, - render: function () { - var flow = this.props.flow; - var status; - if (flow.response) { - status = flow.response.code; - } else { - status = null; - } - return React.createElement("td", {className: "col-status"}, status); - } -}); - - -var SizeColumn = React.createClass({displayName: "SizeColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-size " + (this.props.className || "") }), "Size"); - } - }), - sortKeyFun: function(flow){ - var total = flow.request.contentLength; - if (flow.response) { - total += flow.response.contentLength || 0; - } - return total; - } - }, - render: function () { - var flow = this.props.flow; - - var total = flow.request.contentLength; - if (flow.response) { - total += flow.response.contentLength || 0; - } - var size = utils.formatSize(total); - return React.createElement("td", {className: "col-size"}, size); - } -}); - - -var TimeColumn = React.createClass({displayName: "TimeColumn", - statics: { - Title: React.createClass({displayName: "Title", - render: function(){ - return React.createElement("th", React.__spread({}, this.props, {className: "col-time " + (this.props.className || "") }), "Time"); - } - }), - sortKeyFun: function(flow){ - if(flow.response) { - return flow.response.timestamp_end - flow.request.timestamp_start; - } - } - }, +var Request = React.createClass({displayName: "Request", render: function () { var flow = this.props.flow; - var time; - if (flow.response) { - time = utils.formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)); + var first_line = [ + flow.request.method, + flowutils.RequestUtils.pretty_url(flow.request), + "HTTP/" + flow.request.httpversion.join(".") + ].join(" "); + var content = null; + if (flow.request.contentLength > 0) { + content = "Request Content Size: " + utils.formatSize(flow.request.contentLength); } else { - time = "..."; - } - return React.createElement("td", {className: "col-time"}, time); - } -}); - - -var all_columns = [ - TLSColumn, - IconColumn, - PathColumn, - MethodColumn, - StatusColumn, - SizeColumn, - TimeColumn -]; - -module.exports = all_columns; - -},{"../flow/utils.js":17,"../utils.js":20,"react":"react"}],8:[function(require,module,exports){ -var React = require("react"); -var common = require("./common.js"); -var utils = require("../utils.js"); -var _ = require("lodash"); + content = React.createElement("div", {className: "alert alert-info"}, "No Content"); + } -var VirtualScrollMixin = require("./virtualscroll.js"); -var flowtable_columns = require("./flowtable-columns.js"); + //TODO: Styling -var FlowRow = React.createClass({displayName: "FlowRow", + return ( + React.createElement("section", null, + React.createElement("div", {className: "first-line"}, first_line ), + React.createElement(Headers, {message: flow.request}), + React.createElement("hr", null), + content + ) + ); + } +}); + +var Response = React.createClass({displayName: "Response", render: function () { var flow = this.props.flow; - var columns = this.props.columns.map(function (Column) { - return React.createElement(Column, {key: Column.displayName, flow: flow}); - }.bind(this)); - var className = ""; - if (this.props.selected) { - className += " selected"; - } - if (this.props.highlighted) { - className += " highlighted"; - } - if (flow.intercepted) { - className += " intercepted"; - } - if (flow.request) { - className += " has-request"; - } - if (flow.response) { - className += " has-response"; + var first_line = [ + "HTTP/" + flow.response.httpversion.join("."), + flow.response.code, + flow.response.msg + ].join(" "); + var content = null; + if (flow.response.contentLength > 0) { + content = "Response Content Size: " + utils.formatSize(flow.response.contentLength); + } else { + content = React.createElement("div", {className: "alert alert-info"}, "No Content"); } + //TODO: Styling + return ( - React.createElement("tr", {className: className, onClick: this.props.selectFlow.bind(null, flow)}, - columns - )); - }, - shouldComponentUpdate: function (nextProps) { - return true; - // Further optimization could be done here - // by calling forceUpdate on flow updates, selection changes and column changes. - //return ( - //(this.props.columns.length !== nextProps.columns.length) || - //(this.props.selected !== nextProps.selected) - //); + React.createElement("section", null, + React.createElement("div", {className: "first-line"}, first_line ), + React.createElement(Headers, {message: flow.response}), + React.createElement("hr", null), + content + ) + ); } }); -var FlowTableHead = React.createClass({displayName: "FlowTableHead", - getInitialState: function(){ - return { - sortColumn: undefined, - sortDesc: false - }; - }, - onClick: function(Column){ - var sortDesc = this.state.sortDesc; - var hasSort = Column.sortKeyFun; - if(Column === this.state.sortColumn){ - sortDesc = !sortDesc; - this.setState({ - sortDesc: sortDesc - }); - } else { - this.setState({ - sortColumn: hasSort && Column, - sortDesc: false - }) - } - var sortKeyFun; - if(!sortDesc){ - sortKeyFun = Column.sortKeyFun; - } else { - sortKeyFun = hasSort && function(){ - var k = Column.sortKeyFun.apply(this, arguments); - if(_.isString(k)){ - return utils.reverseString(""+k); - } else { - return -k; - } - } - } - this.props.setSortKeyFun(sortKeyFun); - }, +var Error = React.createClass({displayName: "Error", render: function () { - var columns = this.props.columns.map(function (Column) { - var onClick = this.onClick.bind(this, Column); - var className; - if(this.state.sortColumn === Column) { - if(this.state.sortDesc){ - className = "sort-desc"; - } else { - className = "sort-asc"; - } - } - return React.createElement(Column.Title, { - key: Column.displayName, - onClick: onClick, - className: className}); - }.bind(this)); - return React.createElement("thead", null, - React.createElement("tr", null, columns) + var flow = this.props.flow; + return ( + React.createElement("section", null, + React.createElement("div", {className: "alert alert-warning"}, + flow.error.msg, + React.createElement("div", null, + React.createElement("small", null, utils.formatTimeStamp(flow.error.timestamp) ) + ) + ) + ) ); } }); +module.exports = { + Request: Request, + Response: Response, + Error: Error +}; + +},{"../../flow/utils.js":20,"../../utils.js":23,"react":"react"}],11:[function(require,module,exports){ +var React = require("react"); -var ROW_HEIGHT = 32; +var actions = require("../../actions.js"); -var FlowTable = React.createClass({displayName: "FlowTable", - mixins: [common.StickyHeadMixin, common.AutoScrollMixin, VirtualScrollMixin], - getInitialState: function () { - return { - columns: flowtable_columns - }; - }, - _listen: function(view){ - if(!view){ - return; - } - view.addListener("add", this.onChange); - view.addListener("update", this.onChange); - view.addListener("remove", this.onChange); - view.addListener("recalculate", this.onChange); - }, - componentWillMount: function () { - this._listen(this.props.view); - }, - componentWillReceiveProps: function (nextProps) { - if (nextProps.view !== this.props.view) { - if (this.props.view) { - this.props.view.removeListener("add"); - this.props.view.removeListener("update"); - this.props.view.removeListener("remove"); - this.props.view.removeListener("recalculate"); - } - this._listen(nextProps.view); - } - }, - getDefaultProps: function () { - return { - rowHeight: ROW_HEIGHT - }; - }, - onScrollFlowTable: function () { - this.adjustHead(); - this.onScroll(); - }, - onChange: function () { - this.forceUpdate(); +var NavAction = React.createClass({displayName: "NavAction", + onClick: function (e) { + e.preventDefault(); + this.props.onClick(); }, - scrollIntoView: function (flow) { - this.scrollRowIntoView( - this.props.view.index(flow), - this.refs.body.getDOMNode().offsetTop + render: function () { + return ( + React.createElement("a", {title: this.props.title, + href: "#", + className: "nav-action", + onClick: this.onClick}, + React.createElement("i", {className: "fa fa-fw " + this.props.icon}) + ) ); - }, - renderRow: function (flow) { - var selected = (flow === this.props.selected); - var highlighted = - ( - this.props.view._highlight && - this.props.view._highlight[flow.id] - ); + } +}); - return React.createElement(FlowRow, {key: flow.id, - ref: flow.id, - flow: flow, - columns: this.state.columns, - selected: selected, - highlighted: highlighted, - selectFlow: this.props.selectFlow} - ); - }, +var Nav = React.createClass({displayName: "Nav", render: function () { - //console.log("render flowtable", this.state.start, this.state.stop, this.props.selected); - var flows = this.props.view ? this.props.view.list : []; + var flow = this.props.flow; - var rows = this.renderRows(flows); + var tabs = this.props.tabs.map(function (e) { + var str = e.charAt(0).toUpperCase() + e.slice(1); + var className = this.props.active === e ? "active" : ""; + var onClick = function (event) { + this.props.selectTab(e); + event.preventDefault(); + }.bind(this); + return React.createElement("a", {key: e, + href: "#", + className: className, + onClick: onClick}, str); + }.bind(this)); + + var acceptButton = null; + if(flow.intercepted){ + acceptButton = React.createElement(NavAction, {title: "[a]ccept intercepted flow", icon: "fa-play", onClick: actions.FlowActions.accept.bind(null, flow)}); + } + var revertButton = null; + if(flow.modified){ + revertButton = React.createElement(NavAction, {title: "revert changes to flow [V]", icon: "fa-history", onClick: actions.FlowActions.revert.bind(null, flow)}); + } return ( - React.createElement("div", {className: "flow-table", onScroll: this.onScrollFlowTable}, - React.createElement("table", null, - React.createElement(FlowTableHead, {ref: "head", - columns: this.state.columns, - setSortKeyFun: this.props.setSortKeyFun}), - React.createElement("tbody", {ref: "body"}, - this.getPlaceholderTop(flows.length), - rows, - this.getPlaceholderBottom(flows.length) - ) - ) + React.createElement("nav", {ref: "head", className: "nav-tabs nav-tabs-sm"}, + tabs, + React.createElement(NavAction, {title: "[d]elete flow", icon: "fa-trash", onClick: actions.FlowActions.delete.bind(null, flow)}), + React.createElement(NavAction, {title: "[D]uplicate flow", icon: "fa-copy", onClick: actions.FlowActions.duplicate.bind(null, flow)}), + React.createElement(NavAction, {disabled: true, title: "[r]eplay flow", icon: "fa-repeat", onClick: actions.FlowActions.replay.bind(null, flow)}), + acceptButton, + revertButton ) ); } }); -module.exports = FlowTable; - +module.exports = Nav; -},{"../utils.js":20,"./common.js":4,"./flowtable-columns.js":7,"./virtualscroll.js":13,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +},{"../../actions.js":2,"react":"react"}],12:[function(require,module,exports){ var React = require("react"); var Footer = React.createClass({displayName: "Footer", @@ -1600,7 +1625,7 @@ var Footer = React.createClass({displayName: "Footer", module.exports = Footer; -},{"react":"react"}],10:[function(require,module,exports){ +},{"react":"react"}],13:[function(require,module,exports){ var React = require("react"); var $ = require("jquery"); @@ -1992,7 +2017,7 @@ module.exports = { Header: Header } -},{"../actions.js":2,"../filt/filt.js":16,"../utils.js":20,"./common.js":4,"jquery":"jquery","react":"react"}],11:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":19,"../utils.js":23,"./common.js":4,"jquery":"jquery","react":"react"}],14:[function(require,module,exports){ var React = require("react"); var common = require("./common.js"); @@ -2002,7 +2027,7 @@ var toputils = require("../utils.js"); var views = require("../store/view.js"); var Filt = require("../filt/filt.js"); FlowTable = require("./flowtable.js"); -var flowdetail = require("./flowdetail.js"); +var FlowView = require("./flowview/index.js"); var MainView = React.createClass({displayName: "MainView", mixins: [common.Navigation, common.State], @@ -2216,7 +2241,7 @@ var MainView = React.createClass({displayName: "MainView", if (selected) { details = [ React.createElement(common.Splitter, {key: "splitter"}), - React.createElement(flowdetail.FlowDetail, {key: "flowDetails", ref: "flowDetails", flow: selected}) + React.createElement(FlowView, {key: "flowDetails", ref: "flowDetails", flow: selected}) ]; } else { details = null; @@ -2238,7 +2263,7 @@ var MainView = React.createClass({displayName: "MainView", module.exports = MainView; -},{"../actions.js":2,"../filt/filt.js":16,"../store/view.js":19,"../utils.js":20,"./common.js":4,"./flowdetail.js":6,"./flowtable.js":8,"react":"react"}],12:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":19,"../store/view.js":22,"../utils.js":23,"./common.js":4,"./flowtable.js":7,"./flowview/index.js":9,"react":"react"}],15:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -2333,7 +2358,7 @@ module.exports = { routes: routes }; -},{"../actions.js":2,"../store/store.js":18,"./common.js":4,"./eventlog.js":5,"./footer.js":9,"./header.js":10,"./mainview.js":11,"lodash":"lodash","react":"react","react-router":"react-router"}],13:[function(require,module,exports){ +},{"../actions.js":2,"../store/store.js":21,"./common.js":4,"./eventlog.js":5,"./footer.js":12,"./header.js":13,"./mainview.js":14,"lodash":"lodash","react":"react","react-router":"react-router"}],16:[function(require,module,exports){ var React = require("react"); var VirtualScrollMixin = { @@ -2420,7 +2445,7 @@ var VirtualScrollMixin = { module.exports = VirtualScrollMixin; -},{"react":"react"}],14:[function(require,module,exports){ +},{"react":"react"}],17:[function(require,module,exports){ var actions = require("./actions.js"); @@ -2450,7 +2475,7 @@ function Connection(url) { module.exports = Connection; -},{"./actions.js":2}],15:[function(require,module,exports){ +},{"./actions.js":2}],18:[function(require,module,exports){ var flux = require("flux"); @@ -2474,7 +2499,7 @@ module.exports = { AppDispatcher: AppDispatcher }; -},{"flux":"flux"}],16:[function(require,module,exports){ +},{"flux":"flux"}],19:[function(require,module,exports){ module.exports = (function() { /* * Generated by PEG.js 0.8.0. @@ -4250,10 +4275,10 @@ module.exports = (function() { }; })(); -},{"../flow/utils.js":17}],17:[function(require,module,exports){ +},{"../flow/utils.js":20}],20:[function(require,module,exports){ var _ = require("lodash"); -var _MessageUtils = { +var MessageUtils = { getContentType: function (message) { return this.get_first_header(message, /^Content-Type$/i); }, @@ -4295,7 +4320,7 @@ var defaultPorts = { "https": 443 }; -var RequestUtils = _.extend(_MessageUtils, { +var RequestUtils = _.extend(MessageUtils, { pretty_host: function (request) { //FIXME: Add hostheader return request.host; @@ -4309,16 +4334,16 @@ var RequestUtils = _.extend(_MessageUtils, { } }); -var ResponseUtils = _.extend(_MessageUtils, {}); +var ResponseUtils = _.extend(MessageUtils, {}); module.exports = { ResponseUtils: ResponseUtils, - RequestUtils: RequestUtils - -} + RequestUtils: RequestUtils, + MessageUtils: MessageUtils +}; -},{"lodash":"lodash"}],18:[function(require,module,exports){ +},{"lodash":"lodash"}],21:[function(require,module,exports){ var _ = require("lodash"); var $ = require("jquery"); @@ -4501,7 +4526,7 @@ module.exports = { FlowStore: FlowStore }; -},{"../actions.js":2,"../dispatcher.js":15,"../utils.js":20,"events":1,"jquery":"jquery","lodash":"lodash"}],19:[function(require,module,exports){ +},{"../actions.js":2,"../dispatcher.js":18,"../utils.js":23,"events":1,"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){ var EventEmitter = require('events').EventEmitter; var _ = require("lodash"); @@ -4617,7 +4642,7 @@ module.exports = { StoreView: StoreView }; -},{"../utils.js":20,"events":1,"lodash":"lodash"}],20:[function(require,module,exports){ +},{"../utils.js":23,"events":1,"lodash":"lodash"}],23:[function(require,module,exports){ var $ = require("jquery"); var _ = require("lodash"); -- cgit v1.2.3 From 1143552e1690f8b96b3d95381f7f06cbb46ead59 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 22 Mar 2015 00:21:38 +0100 Subject: web: add content views --- libmproxy/web/static/app.css | 10 ++ libmproxy/web/static/app.js | 215 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 192 insertions(+), 33 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/web/static/app.css b/libmproxy/web/static/app.css index 4f24ddd9..cf2db2c6 100644 --- a/libmproxy/web/static/app.css +++ b/libmproxy/web/static/app.css @@ -290,6 +290,9 @@ header .menu { max-height: 100px; overflow-y: auto; } +.view-selector { + margin-top: 10px; +} .flow-detail table { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; width: 100%; @@ -316,6 +319,13 @@ header .menu { text-overflow: ellipsis; white-space: nowrap; } +.flowview-image { + text-align: center; +} +.flowview-image img { + max-width: 100%; + max-height: 100%; +} .eventlog { height: 200px; flex: 0 0 auto; diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index 04d6f282..eb8ef45e 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -443,7 +443,7 @@ $(function () { -},{"./components/proxyapp.js":15,"./connection":17,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ +},{"./components/proxyapp.js":16,"./connection":18,"jquery":"jquery","react":"react","react-router":"react-router"}],4:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -777,7 +777,7 @@ var EventLog = React.createClass({displayName: "EventLog", module.exports = EventLog; -},{"../actions.js":2,"../store/view.js":22,"./common.js":4,"./virtualscroll.js":16,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ +},{"../actions.js":2,"../store/view.js":23,"./common.js":4,"./virtualscroll.js":17,"lodash":"lodash","react":"react"}],6:[function(require,module,exports){ var React = require("react"); var RequestUtils = require("../flow/utils.js").RequestUtils; var ResponseUtils = require("../flow/utils.js").ResponseUtils; @@ -980,7 +980,7 @@ var all_columns = [ module.exports = all_columns; -},{"../flow/utils.js":20,"../utils.js":23,"react":"react"}],7:[function(require,module,exports){ +},{"../flow/utils.js":21,"../utils.js":24,"react":"react"}],7:[function(require,module,exports){ var React = require("react"); var common = require("./common.js"); var utils = require("../utils.js"); @@ -1180,7 +1180,167 @@ var FlowTable = React.createClass({displayName: "FlowTable", module.exports = FlowTable; -},{"../utils.js":23,"./common.js":4,"./flowtable-columns.js":6,"./virtualscroll.js":16,"lodash":"lodash","react":"react"}],8:[function(require,module,exports){ +},{"../utils.js":24,"./common.js":4,"./flowtable-columns.js":6,"./virtualscroll.js":17,"lodash":"lodash","react":"react"}],8:[function(require,module,exports){ +var React = require("react"); +var _ = require("lodash"); + +var MessageUtils = require("../../flow/utils.js").MessageUtils; +var utils = require("../../utils.js"); + +var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; +var Image = React.createClass({displayName: "Image", + statics: { + matches: function (message) { + return image_regex.test(MessageUtils.getContentType(message)); + } + }, + render: function () { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + var url = "/flows/" + this.props.flow.id + "/" + message_name + "/content"; + return React.createElement("div", {className: "flowview-image"}, + React.createElement("img", {src: url, alt: "preview", className: "img-thumbnail"}) + ); + } +}); + +var Raw = React.createClass({displayName: "Raw", + statics: { + matches: function (message) { + return true; + } + }, + render: function () { + //FIXME + return React.createElement("div", null, "raw"); + } +}); + + +var Auto = React.createClass({displayName: "Auto", + statics: { + matches: function () { + return false; // don't match itself + }, + findView: function (message) { + for (var i = 0; i < all.length; i++) { + if (all[i].matches(message)) { + return all[i]; + } + } + return all[all.length - 1]; + } + }, + render: function () { + var View = Auto.findView(this.props.message); + return React.createElement(View, React.__spread({}, this.props)); + } +}); + +var all = [Auto, Image, Raw]; + + +var ContentEmpty = React.createClass({displayName: "ContentEmpty", + render: function () { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + return React.createElement("div", {className: "alert alert-info"}, "No ", message_name, " content."); + } +}); + +var ContentMissing = React.createClass({displayName: "ContentMissing", + render: function () { + var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; + return React.createElement("div", {className: "alert alert-info"}, message_name, " content missing."); + } +}); + +var TooLarge = React.createClass({displayName: "TooLarge", + render: function () { + var size = utils.formatSize(this.props.message.contentLength); + return React.createElement("div", {className: "alert alert-warning"}, + React.createElement("button", {onClick: this.props.onClick, className: "btn btn-xs btn-warning pull-right"}, "Display anyway"), + size, " content size." + ); + } +}); + +var ViewSelector = React.createClass({displayName: "ViewSelector", + render: function () { + var views = []; + for (var i = 0; i < all.length; i++) { + var view = all[i]; + var className = "btn btn-default"; + if (view === this.props.active) { + className += " active"; + } + var text; + if (view === Auto) { + text = "auto: " + Auto.findView(this.props.message).displayName.toLowerCase(); + } else { + text = view.displayName.toLowerCase(); + } + views.push( + React.createElement("button", { + key: view.displayName, + onClick: this.props.selectView.bind(null, view), + className: className}, + text + ) + ); + } + + return React.createElement("div", {className: "view-selector btn-group btn-group-xs"}, views); + } +}); + +var ContentView = React.createClass({displayName: "ContentView", + getInitialState: function () { + return { + displayLarge: false, + View: Auto + }; + }, + propTypes: { + // It may seem a bit weird at the first glance: + // Every view takes the flow and the message as props, e.g. + // + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + }, + selectView: function (view) { + this.setState({ + View: view + }); + }, + displayLarge: function () { + this.setState({displayLarge: true}); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.setState(this.getInitialState()); + } + }, + render: function () { + var message = this.props.message; + if (message.contentLength === 0) { + return React.createElement(ContentEmpty, React.__spread({}, this.props)); + } else if (message.contentLength === null) { + return React.createElement(ContentMissing, React.__spread({}, this.props)); + } else if (message.contentLength > 1024 * 1024 * 3 && !this.state.displayLarge) { + return React.createElement(TooLarge, React.__spread({}, this.props, {onClick: this.displayLarge})); + } + + return React.createElement("div", null, + React.createElement(this.state.View, React.__spread({}, this.props)), + React.createElement("div", {className: "text-center"}, + React.createElement(ViewSelector, {selectView: this.selectView, active: this.state.View, message: message}) + ) + ); + } +}); + +module.exports = ContentView; + +},{"../../flow/utils.js":21,"../../utils.js":24,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1363,7 +1523,7 @@ var Details = React.createClass({displayName: "Details", module.exports = Details; -},{"../../utils.js":23,"lodash":"lodash","react":"react"}],9:[function(require,module,exports){ +},{"../../utils.js":24,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ var React = require("react"); var _ = require("lodash"); @@ -1439,11 +1599,12 @@ var FlowView = React.createClass({displayName: "FlowView", module.exports = FlowView; -},{"../common.js":4,"./details.js":8,"./messages.js":10,"./nav.js":11,"lodash":"lodash","react":"react"}],10:[function(require,module,exports){ +},{"../common.js":4,"./details.js":9,"./messages.js":11,"./nav.js":12,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){ var React = require("react"); var flowutils = require("../../flow/utils.js"); var utils = require("../../utils.js"); +var ContentView = require("./contentview.js"); var Headers = React.createClass({displayName: "Headers", render: function () { @@ -1473,12 +1634,6 @@ var Request = React.createClass({displayName: "Request", flowutils.RequestUtils.pretty_url(flow.request), "HTTP/" + flow.request.httpversion.join(".") ].join(" "); - var content = null; - if (flow.request.contentLength > 0) { - content = "Request Content Size: " + utils.formatSize(flow.request.contentLength); - } else { - content = React.createElement("div", {className: "alert alert-info"}, "No Content"); - } //TODO: Styling @@ -1487,7 +1642,7 @@ var Request = React.createClass({displayName: "Request", React.createElement("div", {className: "first-line"}, first_line ), React.createElement(Headers, {message: flow.request}), React.createElement("hr", null), - content + React.createElement(ContentView, {flow: flow, message: flow.request}) ) ); } @@ -1501,12 +1656,6 @@ var Response = React.createClass({displayName: "Response", flow.response.code, flow.response.msg ].join(" "); - var content = null; - if (flow.response.contentLength > 0) { - content = "Response Content Size: " + utils.formatSize(flow.response.contentLength); - } else { - content = React.createElement("div", {className: "alert alert-info"}, "No Content"); - } //TODO: Styling @@ -1515,7 +1664,7 @@ var Response = React.createClass({displayName: "Response", React.createElement("div", {className: "first-line"}, first_line ), React.createElement(Headers, {message: flow.response}), React.createElement("hr", null), - content + React.createElement(ContentView, {flow: flow, message: flow.response}) ) ); } @@ -1543,7 +1692,7 @@ module.exports = { Error: Error }; -},{"../../flow/utils.js":20,"../../utils.js":23,"react":"react"}],11:[function(require,module,exports){ +},{"../../flow/utils.js":21,"../../utils.js":24,"./contentview.js":8,"react":"react"}],12:[function(require,module,exports){ var React = require("react"); var actions = require("../../actions.js"); @@ -1606,7 +1755,7 @@ var Nav = React.createClass({displayName: "Nav", module.exports = Nav; -},{"../../actions.js":2,"react":"react"}],12:[function(require,module,exports){ +},{"../../actions.js":2,"react":"react"}],13:[function(require,module,exports){ var React = require("react"); var Footer = React.createClass({displayName: "Footer", @@ -1625,7 +1774,7 @@ var Footer = React.createClass({displayName: "Footer", module.exports = Footer; -},{"react":"react"}],13:[function(require,module,exports){ +},{"react":"react"}],14:[function(require,module,exports){ var React = require("react"); var $ = require("jquery"); @@ -2017,7 +2166,7 @@ module.exports = { Header: Header } -},{"../actions.js":2,"../filt/filt.js":19,"../utils.js":23,"./common.js":4,"jquery":"jquery","react":"react"}],14:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":20,"../utils.js":24,"./common.js":4,"jquery":"jquery","react":"react"}],15:[function(require,module,exports){ var React = require("react"); var common = require("./common.js"); @@ -2263,7 +2412,7 @@ var MainView = React.createClass({displayName: "MainView", module.exports = MainView; -},{"../actions.js":2,"../filt/filt.js":19,"../store/view.js":22,"../utils.js":23,"./common.js":4,"./flowtable.js":7,"./flowview/index.js":9,"react":"react"}],15:[function(require,module,exports){ +},{"../actions.js":2,"../filt/filt.js":20,"../store/view.js":23,"../utils.js":24,"./common.js":4,"./flowtable.js":7,"./flowview/index.js":10,"react":"react"}],16:[function(require,module,exports){ var React = require("react"); var ReactRouter = require("react-router"); var _ = require("lodash"); @@ -2358,7 +2507,7 @@ module.exports = { routes: routes }; -},{"../actions.js":2,"../store/store.js":21,"./common.js":4,"./eventlog.js":5,"./footer.js":12,"./header.js":13,"./mainview.js":14,"lodash":"lodash","react":"react","react-router":"react-router"}],16:[function(require,module,exports){ +},{"../actions.js":2,"../store/store.js":22,"./common.js":4,"./eventlog.js":5,"./footer.js":13,"./header.js":14,"./mainview.js":15,"lodash":"lodash","react":"react","react-router":"react-router"}],17:[function(require,module,exports){ var React = require("react"); var VirtualScrollMixin = { @@ -2445,7 +2594,7 @@ var VirtualScrollMixin = { module.exports = VirtualScrollMixin; -},{"react":"react"}],17:[function(require,module,exports){ +},{"react":"react"}],18:[function(require,module,exports){ var actions = require("./actions.js"); @@ -2475,7 +2624,7 @@ function Connection(url) { module.exports = Connection; -},{"./actions.js":2}],18:[function(require,module,exports){ +},{"./actions.js":2}],19:[function(require,module,exports){ var flux = require("flux"); @@ -2499,7 +2648,7 @@ module.exports = { AppDispatcher: AppDispatcher }; -},{"flux":"flux"}],19:[function(require,module,exports){ +},{"flux":"flux"}],20:[function(require,module,exports){ module.exports = (function() { /* * Generated by PEG.js 0.8.0. @@ -4275,7 +4424,7 @@ module.exports = (function() { }; })(); -},{"../flow/utils.js":20}],20:[function(require,module,exports){ +},{"../flow/utils.js":21}],21:[function(require,module,exports){ var _ = require("lodash"); var MessageUtils = { @@ -4343,7 +4492,7 @@ module.exports = { MessageUtils: MessageUtils }; -},{"lodash":"lodash"}],21:[function(require,module,exports){ +},{"lodash":"lodash"}],22:[function(require,module,exports){ var _ = require("lodash"); var $ = require("jquery"); @@ -4526,7 +4675,7 @@ module.exports = { FlowStore: FlowStore }; -},{"../actions.js":2,"../dispatcher.js":18,"../utils.js":23,"events":1,"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){ +},{"../actions.js":2,"../dispatcher.js":19,"../utils.js":24,"events":1,"jquery":"jquery","lodash":"lodash"}],23:[function(require,module,exports){ var EventEmitter = require('events').EventEmitter; var _ = require("lodash"); @@ -4642,7 +4791,7 @@ module.exports = { StoreView: StoreView }; -},{"../utils.js":23,"events":1,"lodash":"lodash"}],23:[function(require,module,exports){ +},{"../utils.js":24,"events":1,"lodash":"lodash"}],24:[function(require,module,exports){ var $ = require("jquery"); var _ = require("lodash"); -- cgit v1.2.3 From 89d66360d6f7caa9760fe56fa146396b1b4251dc Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 22 Mar 2015 00:28:08 +0100 Subject: tweak css --- libmproxy/web/static/app.css | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'libmproxy') diff --git a/libmproxy/web/static/app.css b/libmproxy/web/static/app.css index cf2db2c6..91e847a4 100644 --- a/libmproxy/web/static/app.css +++ b/libmproxy/web/static/app.css @@ -290,6 +290,9 @@ header .menu { max-height: 100px; overflow-y: auto; } +.flow-detail hr { + margin: 0 0 5px; +} .view-selector { margin-top: 10px; } @@ -309,6 +312,9 @@ header .menu { width: 50%; padding-right: 1em; } +.header-table td { + line-height: 1.3em; +} .header-table .header-name { width: 33%; padding-right: 1em; -- cgit v1.2.3 From 89383e9c138f68caf1cc394174250c133d21aa04 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 13:32:24 +1300 Subject: Refactor status bar prompting to use signal system --- libmproxy/console/__init__.py | 58 +-------- libmproxy/console/common.py | 46 +++----- libmproxy/console/flowlist.py | 112 ++++++++++-------- libmproxy/console/flowview.py | 109 ++++++++++------- libmproxy/console/grideditor.py | 31 +++-- libmproxy/console/signals.py | 16 ++- libmproxy/console/statusbar.py | 84 +++++++++++-- libmproxy/console/window.py | 255 +++++++++++++++++++--------------------- 8 files changed, 381 insertions(+), 330 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index aae7a9c4..d8eb8a41 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -272,7 +272,7 @@ class ConsoleMaster(flow.FlowMaster): self.eventlog = not self.eventlog self.view_flowlist() - def _readflow(self, paths): + def _readflows(self, path): """ Utitility function that reads a list of flows or prints an error to the UI if that fails. @@ -281,7 +281,7 @@ class ConsoleMaster(flow.FlowMaster): - a list of flows, otherwise. """ try: - return flow.read_flows_from_paths(paths) + return flow.read_flows_from_paths([path]) except flow.FlowReadError as e: if not self.statusbar: print >> sys.stderr, e.strerror @@ -291,12 +291,12 @@ class ConsoleMaster(flow.FlowMaster): return None def client_playback_path(self, path): - flows = self._readflow(path) + flows = self._readflows(path) if flows: self.start_client_playback(flows, False) def server_playback_path(self, path): - flows = self._readflow(path) + flows = self._readflows(path) if flows: self.start_server_playback( flows, @@ -387,7 +387,6 @@ class ConsoleMaster(flow.FlowMaster): self.header = None self.body = None self.help_context = None - self.prompting = False self.onekey = False self.loop = urwid.MainLoop( self.view, @@ -538,55 +537,6 @@ class ConsoleMaster(flow.FlowMaster): self.sync_list_view() return reterr - 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 = set(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") - signals.status_message.send(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: - signals.status_message.send(message=msg, expire=1) - - def prompt_cancel(self): - self.prompt_done() - def accept_all(self): self.state.accept_all(self) diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 9731b682..185480db 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -203,13 +203,11 @@ def save_data(path, data, master, state): def ask_save_path(prompt, data, master, state): - master.path_prompt( - prompt, - state.last_saveload, - save_data, - data, - master, - state + signals.status_path_prompt.send( + prompt = prompt, + text = state.last_saveload, + callback = save_data, + args = (data, master, state) ) @@ -263,14 +261,13 @@ def copy_flow(part, scope, flow, master, state): def save(k): if k == "y": ask_save_path("Save data: ", data, master, state) - - master.prompt_onekey( - "Cannot copy binary data to clipboard. Save as file?", - ( + signals.status_prompt_onekey.send( + prompt = "Cannot copy binary data to clipboard. Save as file?", + keys = ( ("yes", "y"), ("no", "n"), ), - save + callback = save ) @@ -282,14 +279,11 @@ def ask_copy_part(scope, flow, master, state): if scope != "s": choices.append(("url", "u")) - master.prompt_onekey( - "Copy", - choices, - copy_flow, - scope, - flow, - master, - state + signals.status_prompt_onekey.send( + prompt = "Copy", + keys = choices, + callback = copy_flow, + args = (scope, flow, master, state) ) @@ -306,16 +300,14 @@ def ask_save_body(part, master, state, flow): # We first need to determine whether we want to save the request or the # response content. if request_has_content and response_has_content: - master.prompt_onekey( - "Save", - ( + signals.status_prompt_onekey.send( + prompt = "Save", + keys = ( ("request", "q"), ("response", "s"), ), - ask_save_body, - master, - state, - flow + callback = ask_save_body, + args = (master, state, flow) ) elif response_has_content: ask_save_body("s", master, state, flow) diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index c8ecf15c..d4dd89d8 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -111,17 +111,17 @@ class ConnectionItem(urwid.WidgetWrap): def save_flows_prompt(self, k): if k == "a": - self.master.path_prompt( - "Save all flows to: ", - self.state.last_saveload, - self.master.save_flows + signals.status_path_prompt.send( + prompt = "Save all flows to: ", + text = self.state.last_saveload, + callback = self.master.save_flows ) else: - self.master.path_prompt( - "Save this flow to: ", - self.state.last_saveload, - self.master.save_one_flow, - self.flow + signals.status_path_prompt.send( + prompt = "Save this flow to: ", + text = self.state.last_saveload, + callback = self.master.save_one_flow, + args = (self.flow,) ) def stop_server_playback_prompt(self, a): @@ -150,10 +150,10 @@ class ConnectionItem(urwid.WidgetWrap): self.master.options.replay_ignore_host ) else: - self.master.path_prompt( - "Server replay path: ", - self.state.last_saveload, - self.master.server_playback_path + signals.status_path_prompt.send( + prompt = "Server replay path: ", + text = self.state.last_saveload, + callback = self.master.server_playback_path ) def keypress(self, (maxcol,), key): @@ -175,23 +175,23 @@ class ConnectionItem(urwid.WidgetWrap): self.master.sync_list_view() elif key == "S": if not self.master.server_playback: - self.master.prompt_onekey( - "Server Replay", - ( + signals.status_prompt_onekey.send( + prompt = "Server Replay", + keys = ( ("all flows", "a"), ("this flow", "t"), ("file", "f"), ), - self.server_replay_prompt, + callback = self.server_replay_prompt, ) else: - self.master.prompt_onekey( - "Stop current server replay?", - ( + signals.status_prompt_onekey.send( + prompt = "Stop current server replay?", + keys = ( ("yes", "y"), ("no", "n"), ), - self.stop_server_playback_prompt, + callback = self.stop_server_playback_prompt, ) elif key == "V": if not self.flow.modified(): @@ -201,13 +201,14 @@ class ConnectionItem(urwid.WidgetWrap): self.master.sync_list_view() signals.status_message.send(message="Reverted.") elif key == "w": - self.master.prompt_onekey( - "Save", - ( + signals.status_prompt_onekey.send( + self, + prompt = "Save", + keys = ( ("all flows", "a"), ("this flow", "t"), ), - self.save_flows_prompt, + callback = self.save_flows_prompt, ) elif key == "X": self.flow.kill(self.master) @@ -215,11 +216,11 @@ class ConnectionItem(urwid.WidgetWrap): 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 + signals.status_path_prompt.send( + prompt = "Send flow to script: ", + text = self.state.last_script, + callback = self.master.run_script_once, + args = (self.flow,) ) elif key == "g": common.ask_copy_part("a", self.flow, self.master, self.state) @@ -266,7 +267,12 @@ class FlowListBox(urwid.ListBox): def get_method(self, k): if k == "e": - self.master.prompt("Method:", "", self.get_method_raw) + signals.status_prompt.send( + self, + prompt = "Method:", + text = "", + callback = self.get_method_raw + ) else: method = "" for i in common.METHOD_OPTIONS: @@ -275,11 +281,11 @@ class FlowListBox(urwid.ListBox): self.get_url(method) def get_url(self, method): - self.master.prompt( - "URL:", - "http://www.example.com/", - self.new_request, - method + signals.status_prompt.send( + prompt = "URL:", + text = "http://www.example.com/", + callback = self.new_request, + args = (method,) ) def new_request(self, url, method): @@ -301,22 +307,23 @@ class FlowListBox(urwid.ListBox): elif key == "e": self.master.toggle_eventlog() elif key == "l": - self.master.prompt( - "Limit: ", - self.master.state.limit_txt, - self.master.set_limit + signals.status_prompt.send( + prompt = "Limit: ", + text = self.master.state.limit_txt, + callback = self.master.set_limit ) elif key == "L": - self.master.path_prompt( - "Load flows: ", - self.master.state.last_saveload, - self.master.load_flows_callback + signals.status_path_prompt.send( + self, + prompt = "Load flows: ", + text = self.master.state.last_saveload, + callback = self.master.load_flows_callback ) elif key == "n": - self.master.prompt_onekey( - "Method", - common.METHOD_OPTIONS, - self.get_method + signals.status_prompt_onekey.send( + prompt = "Method", + keys = common.METHOD_OPTIONS, + callback = self.get_method ) elif key == "F": self.master.toggle_follow_flows() @@ -324,10 +331,11 @@ class FlowListBox(urwid.ListBox): if self.master.stream: self.master.stop_stream() else: - self.master.path_prompt( - "Stream flows to: ", - self.master.state.last_saveload, - self.master.start_stream_to_path + signals.status_path_prompt.send( + self, + prompt = "Stream flows to: ", + text = self.master.state.last_saveload, + callback = self.master.start_stream_to_path ) else: return urwid.ListBox.keypress(self, size, key) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index b22bbb37..941ceb94 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -492,7 +492,11 @@ class FlowView(urwid.WidgetWrap): def edit_method(self, m): if m == "e": - self.master.prompt_edit("Method", self.flow.request.method, self.set_method_raw) + signals.status_prompt.send( + prompt = "Method: ", + text = self.flow.request.method, + callback = self.set_method_raw + ) else: for i in common.METHOD_OPTIONS: if i[1] == m: @@ -567,14 +571,14 @@ class FlowView(urwid.WidgetWrap): message.content = c.rstrip("\n") elif part == "f": if not message.get_form_urlencoded() and message.content: - self.master.prompt_onekey( - "Existing body is not a URL-encoded form. Clear and edit?", - [ + signals.status_prompt_onekey.send( + prompt = "Existing body is not a URL-encoded form. Clear and edit?", + keys = [ ("yes", "y"), ("no", "n"), ], - self.edit_form_confirm, - message + callback = self.edit_form_confirm, + args = (message,) ) else: self.edit_form(message) @@ -587,13 +591,29 @@ class FlowView(urwid.WidgetWrap): elif part == "q": self.master.view_grideditor(grideditor.QueryEditor(self.master, message.get_query().lst, self.set_query, message)) elif part == "u" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.prompt_edit("URL", message.url, self.set_url) + signals.status_prompt.send( + prompt = "URL: ", + text = message.url, + callback = self.set_url + ) elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.prompt_onekey("Method", common.METHOD_OPTIONS, self.edit_method) + signals.status_prompt_onekey.send( + prompt = "Method", + keys = common.METHOD_OPTIONS, + callback = self.edit_method + ) elif part == "c" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: - self.master.prompt_edit("Code", str(message.code), self.set_resp_code) + signals.status_prompt.send( + prompt = "Code: ", + text = str(message.code), + callback = self.set_resp_code + ) elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: - self.master.prompt_edit("Message", message.msg, self.set_resp_msg) + signals.status_prompt.send( + prompt = "Message: ", + text = message.msg, + callback = self.set_resp_msg + ) self.master.refresh_flow(self.flow) def _view_nextprev_flow(self, np, flow): @@ -684,9 +704,9 @@ class FlowView(urwid.WidgetWrap): signals.status_message.send(message="Duplicated.") elif key == "e": if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: - self.master.prompt_onekey( - "Edit request", - ( + signals.status_prompt_onekey.send( + prompt = "Edit request", + keys = ( ("query", "q"), ("path", "p"), ("url", "u"), @@ -695,18 +715,18 @@ class FlowView(urwid.WidgetWrap): ("raw body", "r"), ("method", "m"), ), - self.edit + callback = self.edit ) else: - self.master.prompt_onekey( - "Edit response", - ( + signals.status_prompt_onekey.send( + prompt = "Edit response", + keys = ( ("code", "c"), ("message", "m"), ("header", "h"), ("raw body", "r"), ), - self.edit + callback = self.edit ) key = None elif key == "f": @@ -727,10 +747,11 @@ class FlowView(urwid.WidgetWrap): elif key == "m": p = list(contentview.view_prompts) p.insert(0, ("Clear", "C")) - self.master.prompt_onekey( - "Display mode", - p, - self.change_this_display_mode + signals.status_prompt_onekey.send( + self, + prompt = "Display mode", + keys = p, + callback = self.change_this_display_mode ) key = None elif key == "p": @@ -748,11 +769,11 @@ class FlowView(urwid.WidgetWrap): self.master.refresh_flow(self.flow) signals.status_message.send(message="Reverted.") elif key == "W": - self.master.path_prompt( - "Save this flow: ", - self.state.last_saveload, - self.master.save_one_flow, - self.flow + signals.status_path_prompt.send( + prompt = "Save this flow: ", + text = self.state.last_saveload, + callback = self.master.save_one_flow, + args = (self.flow,) ) elif key == "v": if conn and conn.content: @@ -763,18 +784,20 @@ class FlowView(urwid.WidgetWrap): else: signals.status_message.send(message="Error! Set $EDITOR or $PAGER.") elif key == "|": - self.master.path_prompt( - "Send flow to script: ", self.state.last_script, - self.master.run_script_once, self.flow + signals.status_path_prompt.send( + prompt = "Send flow to script: ", + text = self.state.last_script, + callback = self.master.run_script_once, + args = (self.flow,) ) elif key == "x": - self.master.prompt_onekey( - "Delete body", - ( + signals.status_prompt_onekey.send( + prompt = "Delete body", + keys = ( ("completely", "c"), ("mark as missing", "m"), ), - self.delete_body + callback = self.delete_body ) key = None elif key == "X": @@ -787,22 +810,24 @@ class FlowView(urwid.WidgetWrap): if not conn.decode(): signals.status_message.send(message="Could not decode - invalid data?") else: - self.master.prompt_onekey( - "Select encoding: ", - ( + signals.status_prompt_onekey.send( + prompt = "Select encoding: ", + keys = ( ("gzip", "z"), ("deflate", "d"), ), - self.encode_callback, - conn + callback = self.encode_callback, + args = (conn,) ) self.master.refresh_flow(self.flow) elif key == "/": last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") search_prompt = "Search body ["+last_search_string+"]: " if last_search_string else "Search body: " - self.master.prompt(search_prompt, - None, - self.search) + signals.status_prompt.send( + prompt = search_prompt, + text = "", + callback = self.search + ) elif key == "n": self.search_again(backwards=False) elif key == "N": diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index 0b563c52..eb66e59e 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -338,11 +338,20 @@ class GridEditor(urwid.WidgetWrap): self.walker.delete_focus() elif key == "r": if self.walker.get_current_value() is not None: - self.master.path_prompt("Read file: ", "", self.read_file) + signals.status_path_prompt.send( + self, + prompt = "Read file: ", + text = "", + callback = self.read_file + ) elif key == "R": if self.walker.get_current_value() is not None: - self.master.path_prompt( - "Read unescaped file: ", "", self.read_file, True + signals.status_path_prompt.send( + self, + prompt = "Read unescaped file: ", + text = "", + callback = self.read_file, + args = (True,) ) elif key == "e": o = self.walker.get_current_value() @@ -431,10 +440,10 @@ class HeaderEditor(GridEditor): def handle_key(self, key): if key == "U": - self.master.prompt_onekey( - "Add User-Agent header:", - [(i[0], i[1]) for i in http_uastrings.UASTRINGS], - self.set_user_agent, + signals.status_prompt_onekey.send( + prompt = "Add User-Agent header:", + keys = [(i[0], i[1]) for i in http_uastrings.UASTRINGS], + callback = self.set_user_agent, ) return True @@ -500,10 +509,10 @@ class SetHeadersEditor(GridEditor): def handle_key(self, key): if key == "U": - self.master.prompt_onekey( - "Add User-Agent header:", - [(i[0], i[1]) for i in http_uastrings.UASTRINGS], - self.set_user_agent, + signals.status_prompt_onekey.send( + prompt = "Add User-Agent header:", + keys = [(i[0], i[1]) for i in http_uastrings.UASTRINGS], + callback = self.set_user_agent, ) return True diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py index 7b0ec937..8fb35cff 100644 --- a/libmproxy/console/signals.py +++ b/libmproxy/console/signals.py @@ -1,5 +1,19 @@ - import blinker +# Show a status message in the action bar status_message = blinker.Signal() + +# Prompt for input +status_prompt = blinker.Signal() + +# Prompt for a path +status_path_prompt = blinker.Signal() + +# Prompt for a single keystroke +status_prompt_onekey = blinker.Signal() + +# Call a callback in N seconds call_in = blinker.Signal() + +# Focus the body, footer or header of the main window +focus = blinker.Signal() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index a29767e4..c1a907bd 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -2,7 +2,7 @@ import time import urwid -from . import pathedit, signals +from . import pathedit, signals, common from .. import utils @@ -11,18 +11,12 @@ class ActionBar(urwid.WidgetWrap): urwid.WidgetWrap.__init__(self, None) self.clear() signals.status_message.connect(self.sig_message) + signals.status_prompt.connect(self.sig_prompt) + signals.status_path_prompt.connect(self.sig_path_prompt) + signals.status_prompt_onekey.connect(self.sig_prompt_onekey) - def clear(self): - self._w = urwid.Text("") - - def selectable(self): - return True - - def path_prompt(self, prompt, text): - self._w = pathedit.PathEdit(prompt, text) - - def prompt(self, prompt, text = ""): - self._w = urwid.Edit(prompt, text or "") + self.prompting = False + self.onekey = False def sig_message(self, sender, message, expire=None): w = urwid.Text(message) @@ -33,6 +27,72 @@ class ActionBar(urwid.WidgetWrap): self.clear() signals.call_in.send(seconds=expire, callback=cb) + def sig_prompt(self, sender, prompt, text, callback, args=()): + signals.focus.send(self, section="footer") + self._w = urwid.Edit(prompt, text or "") + self.prompting = (callback, args) + + def sig_path_prompt(self, sender, prompt, text, callback, args=()): + signals.focus.send(self, section="footer") + self._w = pathedit.PathEdit(prompt, text) + self.prompting = (callback, args) + + def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): + """ + Keys are a set of (word, key) tuples. The appropriate key in the + word is highlighted. + """ + signals.focus.send(self, section="footer") + 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 = set(i[1] for i in keys) + self._w = urwid.Edit(prompt, "") + self.prompting = (callback, args) + + def selectable(self): + return True + + def keypress(self, size, k): + if self.prompting: + if k == "esc": + self.prompt_done() + elif self.onekey: + if k == "enter": + self.prompt_done() + elif k in self.onekey: + self.prompt_execute(k) + elif k == "enter": + self.prompt_execute() + else: + if common.is_keypress(k): + self._w.keypress(size, k) + else: + return k + + def clear(self): + self._w = urwid.Text("") + + def prompt_done(self): + self.prompting = False + self.onekey = False + signals.status_message.send(message="") + signals.focus.send(self, section="body") + + def prompt_execute(self, txt=None): + if not txt: + txt = self._w.get_edit_text() + p, args = self.prompting + self.prompt_done() + msg = p(txt, *args) + if msg: + signals.status_message.send(message=msg, expire=1) + class StatusBar(urwid.WidgetWrap): def __init__(self, master, helptext): diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index 44a5a316..55145c48 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -1,151 +1,144 @@ import urwid -from . import common, grideditor +from . import common, grideditor, signals, contentview class Window(urwid.Frame): def __init__(self, master, body, header, footer): urwid.Frame.__init__(self, body, header=header, footer=footer) self.master = master + signals.focus.connect(self.sig_focus) + + def sig_focus(self, sender, section): + self.focus_position = section def keypress(self, size, k): - if self.master.prompting: - if k == "esc": - self.master.prompt_cancel() - elif self.master.onekey: - if k == "enter": - self.master.prompt_cancel() - elif k in self.master.onekey: - self.master.prompt_execute(k) - elif k == "enter": - self.master.prompt_execute() - else: - if common.is_keypress(k): - urwid.Frame.keypress(self, self.master.loop.screen_size, k) - else: - return k - else: - k = urwid.Frame.keypress(self, self.master.loop.screen_size, k) - if k == "?": - self.master.view_help() - elif k == "c": - if not self.master.client_playback: - self.master.path_prompt( - "Client replay: ", - self.master.state.last_saveload, - self.master.client_playback_path - ) - else: - self.master.prompt_onekey( - "Stop current client replay?", - ( - ("yes", "y"), - ("no", "n"), - ), - self.master.stop_client_playback_prompt, - ) - elif k == "H": - self.master.view_grideditor( - grideditor.SetHeadersEditor( - self.master, - self.master.setheaders.get_specs(), - self.master.setheaders.set - ) - ) - elif k == "I": - self.master.view_grideditor( - grideditor.HostPatternEditor( - self.master, - [[x] for x in self.master.get_ignore_filter()], - self.master.edit_ignore_filter - ) + k = urwid.Frame.keypress(self, self.master.loop.screen_size, k) + if k == "?": + self.master.view_help() + elif k == "c": + if not self.master.client_playback: + signals.status_path_prompt.send( + self, + prompt = "Client replay: ", + text = self.master.state.last_saveload, + callback = self.master.client_playback_path ) - elif k == "T": - self.master.view_grideditor( - grideditor.HostPatternEditor( - self.master, - [[x] for x in self.master.get_tcp_filter()], - self.master.edit_tcp_filter - ) - ) - elif k == "i": - self.master.prompt( - "Intercept filter: ", - self.master.state.intercept_txt, - self.master.set_intercept - ) - elif k == "Q": - raise urwid.ExitMainLoop - elif k == "q": - self.master.prompt_onekey( - "Quit", - ( + else: + signals.status_prompt_onekey.send( + self, + prompt = "Stop current client replay?", + keys = ( ("yes", "y"), ("no", "n"), ), - self.master.quit, + callback = self.master.stop_client_playback_prompt, ) - elif k == "M": - self.master.prompt_onekey( - "Global default display mode", - contentview.view_prompts, - self.master.change_default_display_mode + elif k == "H": + self.master.view_grideditor( + grideditor.SetHeadersEditor( + self.master, + self.master.setheaders.get_specs(), + self.master.setheaders.set ) - elif k == "R": - self.master.view_grideditor( - grideditor.ReplaceEditor( - self.master, - self.master.replacehooks.get_specs(), - self.master.replacehooks.set - ) + ) + elif k == "I": + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + [[x] for x in self.master.get_ignore_filter()], + self.master.edit_ignore_filter ) - elif k == "s": - self.master.view_grideditor( - grideditor.ScriptEditor( - self.master, - [[i.command] for i in self.master.scripts], - self.master.edit_scripts - ) + ) + elif k == "T": + self.master.view_grideditor( + grideditor.HostPatternEditor( + self.master, + [[x] for x in self.master.get_tcp_filter()], + self.master.edit_tcp_filter ) - elif k == "S": - if not self.master.server_playback: - self.master.path_prompt( - "Server replay path: ", - self.master.state.last_saveload, - self.master.server_playback_path - ) - else: - self.master.prompt_onekey( - "Stop current server replay?", - ( - ("yes", "y"), - ("no", "n"), - ), - self.master.stop_server_playback_prompt, - ) - elif k == "o": - self.master.prompt_onekey( - "Options", - ( - ("anticache", "a"), - ("anticomp", "c"), - ("showhost", "h"), - ("killextra", "k"), - ("norefresh", "n"), - ("no-upstream-certs", "u"), - ), - self.master._change_options + ) + elif k == "i": + signals.status_prompt.send( + self, + prompt = "Intercept filter: ", + text = self.master.state.intercept_txt, + callback = self.master.set_intercept + ) + elif k == "Q": + raise urwid.ExitMainLoop + elif k == "q": + signals.status_prompt_onekey.send( + self, + prompt = "Quit", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.master.quit, + ) + elif k == "M": + signals.status_prompt_onekey.send( + prompt = "Global default display mode", + keys = contentview.view_prompts, + callback = self.master.change_default_display_mode + ) + elif k == "R": + self.master.view_grideditor( + grideditor.ReplaceEditor( + self.master, + self.master.replacehooks.get_specs(), + self.master.replacehooks.set ) - elif k == "t": - self.master.prompt( - "Sticky cookie filter: ", - self.master.stickycookie_txt, - self.master.set_stickycookie + ) + elif k == "s": + self.master.view_grideditor( + grideditor.ScriptEditor( + self.master, + [[i.command] for i in self.master.scripts], + self.master.edit_scripts ) - elif k == "u": - self.master.prompt( - "Sticky auth filter: ", - self.master.stickyauth_txt, - self.master.set_stickyauth + ) + elif k == "S": + if not self.master.server_playback: + signals.status_path_prompt.send( + self, + prompt = "Server replay path: ", + text = self.master.state.last_saveload, + callback = self.master.server_playback_path ) else: - return k - self.footer.redraw() + signals.status_prompt_onekey.send( + self, + prompt = "Stop current server replay?", + keys = ( + ("yes", "y"), + ("no", "n"), + ), + callback = self.master.stop_server_playback_prompt, + ) + elif k == "o": + signals.status_prompt_onekey.send( + prompt = "Options", + keys = ( + ("anticache", "a"), + ("anticomp", "c"), + ("showhost", "h"), + ("killextra", "k"), + ("norefresh", "n"), + ("no-upstream-certs", "u"), + ), + callback = self.master._change_options + ) + elif k == "t": + signals.status_prompt.send( + prompt = "Sticky cookie filter: ", + text = self.master.stickycookie_txt, + callback = self.master.set_stickycookie + ) + elif k == "u": + signals.status_prompt.send( + prompt = "Sticky auth filter: ", + text = self.master.stickyauth_txt, + callback = self.master.set_stickyauth + ) + else: + return k -- cgit v1.2.3 From 572000aa039a789ba35d4ef14e0c096256d6997d Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 13:59:34 +1300 Subject: Rationalise prompt calling conventions --- libmproxy/console/common.py | 8 ++++---- libmproxy/console/flowlist.py | 30 +++++++++++++++--------------- libmproxy/console/flowview.py | 18 +++++++++--------- libmproxy/console/grideditor.py | 8 ++++---- libmproxy/console/signals.py | 2 +- libmproxy/console/statusbar.py | 18 ++++++------------ libmproxy/console/window.py | 14 +++++++------- 7 files changed, 46 insertions(+), 52 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 185480db..e4ecde91 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -203,7 +203,7 @@ def save_data(path, data, master, state): def ask_save_path(prompt, data, master, state): - signals.status_path_prompt.send( + signals.status_prompt_path.send( prompt = prompt, text = state.last_saveload, callback = save_data, @@ -260,7 +260,7 @@ def copy_flow(part, scope, flow, master, state): except RuntimeError: def save(k): if k == "y": - ask_save_path("Save data: ", data, master, state) + ask_save_path("Save data", data, master, state) signals.status_prompt_onekey.send( prompt = "Cannot copy binary data to clipboard. Save as file?", keys = ( @@ -316,14 +316,14 @@ def ask_save_body(part, master, state, flow): elif part == "q" and request_has_content: ask_save_path( - "Save request content: ", + "Save request content", flow.request.get_decoded_content(), master, state ) elif part == "s" and response_has_content: ask_save_path( - "Save response content: ", + "Save response content", flow.response.get_decoded_content(), master, state diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index d4dd89d8..f39188bb 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -111,14 +111,14 @@ class ConnectionItem(urwid.WidgetWrap): def save_flows_prompt(self, k): if k == "a": - signals.status_path_prompt.send( - prompt = "Save all flows to: ", + signals.status_prompt_path.send( + prompt = "Save all flows to", text = self.state.last_saveload, callback = self.master.save_flows ) else: - signals.status_path_prompt.send( - prompt = "Save this flow to: ", + signals.status_prompt_path.send( + prompt = "Save this flow to", text = self.state.last_saveload, callback = self.master.save_one_flow, args = (self.flow,) @@ -150,8 +150,8 @@ class ConnectionItem(urwid.WidgetWrap): self.master.options.replay_ignore_host ) else: - signals.status_path_prompt.send( - prompt = "Server replay path: ", + signals.status_prompt_path.send( + prompt = "Server replay path", text = self.state.last_saveload, callback = self.master.server_playback_path ) @@ -216,8 +216,8 @@ class ConnectionItem(urwid.WidgetWrap): if self.flow.request: self.master.view_flow(self.flow) elif key == "|": - signals.status_path_prompt.send( - prompt = "Send flow to script: ", + signals.status_prompt_path.send( + prompt = "Send flow to script", text = self.state.last_script, callback = self.master.run_script_once, args = (self.flow,) @@ -269,7 +269,7 @@ class FlowListBox(urwid.ListBox): if k == "e": signals.status_prompt.send( self, - prompt = "Method:", + prompt = "Method", text = "", callback = self.get_method_raw ) @@ -282,7 +282,7 @@ class FlowListBox(urwid.ListBox): def get_url(self, method): signals.status_prompt.send( - prompt = "URL:", + prompt = "URL", text = "http://www.example.com/", callback = self.new_request, args = (method,) @@ -308,14 +308,14 @@ class FlowListBox(urwid.ListBox): self.master.toggle_eventlog() elif key == "l": signals.status_prompt.send( - prompt = "Limit: ", + prompt = "Limit", text = self.master.state.limit_txt, callback = self.master.set_limit ) elif key == "L": - signals.status_path_prompt.send( + signals.status_prompt_path.send( self, - prompt = "Load flows: ", + prompt = "Load flows", text = self.master.state.last_saveload, callback = self.master.load_flows_callback ) @@ -331,9 +331,9 @@ class FlowListBox(urwid.ListBox): if self.master.stream: self.master.stop_stream() else: - signals.status_path_prompt.send( + signals.status_prompt_path.send( self, - prompt = "Stream flows to: ", + prompt = "Stream flows to", text = self.master.state.last_saveload, callback = self.master.start_stream_to_path ) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 941ceb94..b9d5fbca 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -493,7 +493,7 @@ class FlowView(urwid.WidgetWrap): def edit_method(self, m): if m == "e": signals.status_prompt.send( - prompt = "Method: ", + prompt = "Method", text = self.flow.request.method, callback = self.set_method_raw ) @@ -592,7 +592,7 @@ class FlowView(urwid.WidgetWrap): self.master.view_grideditor(grideditor.QueryEditor(self.master, message.get_query().lst, self.set_query, message)) elif part == "u" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: signals.status_prompt.send( - prompt = "URL: ", + prompt = "URL", text = message.url, callback = self.set_url ) @@ -604,13 +604,13 @@ class FlowView(urwid.WidgetWrap): ) elif part == "c" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: signals.status_prompt.send( - prompt = "Code: ", + prompt = "Code", text = str(message.code), callback = self.set_resp_code ) elif part == "m" and self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE: signals.status_prompt.send( - prompt = "Message: ", + prompt = "Message", text = message.msg, callback = self.set_resp_msg ) @@ -769,8 +769,8 @@ class FlowView(urwid.WidgetWrap): self.master.refresh_flow(self.flow) signals.status_message.send(message="Reverted.") elif key == "W": - signals.status_path_prompt.send( - prompt = "Save this flow: ", + signals.status_prompt_path.send( + prompt = "Save this flow", text = self.state.last_saveload, callback = self.master.save_one_flow, args = (self.flow,) @@ -784,8 +784,8 @@ class FlowView(urwid.WidgetWrap): else: signals.status_message.send(message="Error! Set $EDITOR or $PAGER.") elif key == "|": - signals.status_path_prompt.send( - prompt = "Send flow to script: ", + signals.status_prompt_path.send( + prompt = "Send flow to script", text = self.state.last_script, callback = self.master.run_script_once, args = (self.flow,) @@ -822,7 +822,7 @@ class FlowView(urwid.WidgetWrap): self.master.refresh_flow(self.flow) elif key == "/": last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") - search_prompt = "Search body ["+last_search_string+"]: " if last_search_string else "Search body: " + search_prompt = "Search body ["+last_search_string+"]" if last_search_string else "Search body" signals.status_prompt.send( prompt = search_prompt, text = "", diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index eb66e59e..e7c9854b 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -338,17 +338,17 @@ class GridEditor(urwid.WidgetWrap): self.walker.delete_focus() elif key == "r": if self.walker.get_current_value() is not None: - signals.status_path_prompt.send( + signals.status_prompt_path.send( self, - prompt = "Read file: ", + prompt = "Read file", text = "", callback = self.read_file ) elif key == "R": if self.walker.get_current_value() is not None: - signals.status_path_prompt.send( + signals.status_prompt_path.send( self, - prompt = "Read unescaped file: ", + prompt = "Read unescaped file", text = "", callback = self.read_file, args = (True,) diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py index 8fb35cff..e8944afb 100644 --- a/libmproxy/console/signals.py +++ b/libmproxy/console/signals.py @@ -7,7 +7,7 @@ status_message = blinker.Signal() status_prompt = blinker.Signal() # Prompt for a path -status_path_prompt = blinker.Signal() +status_prompt_path = blinker.Signal() # Prompt for a single keystroke status_prompt_onekey = blinker.Signal() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index c1a907bd..7ff26b15 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -12,7 +12,7 @@ class ActionBar(urwid.WidgetWrap): self.clear() signals.status_message.connect(self.sig_message) signals.status_prompt.connect(self.sig_prompt) - signals.status_path_prompt.connect(self.sig_path_prompt) + signals.status_prompt_path.connect(self.sig_path_prompt) signals.status_prompt_onekey.connect(self.sig_prompt_onekey) self.prompting = False @@ -27,14 +27,17 @@ class ActionBar(urwid.WidgetWrap): self.clear() signals.call_in.send(seconds=expire, callback=cb) + def prep_prompt(self, p): + return p.strip() + ": " + def sig_prompt(self, sender, prompt, text, callback, args=()): signals.focus.send(self, section="footer") - self._w = urwid.Edit(prompt, text or "") + self._w = urwid.Edit(self.prep_prompt(prompt), text or "") self.prompting = (callback, args) def sig_path_prompt(self, sender, prompt, text, callback, args=()): signals.focus.send(self, section="footer") - self._w = pathedit.PathEdit(prompt, text) + self._w = pathedit.PathEdit(self.prep_prompt(prompt), text) self.prompting = (callback, args) def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): @@ -230,12 +233,3 @@ class StatusBar(urwid.WidgetWrap): 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) diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index 55145c48..87f06637 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -16,9 +16,9 @@ class Window(urwid.Frame): self.master.view_help() elif k == "c": if not self.master.client_playback: - signals.status_path_prompt.send( + signals.status_prompt_path.send( self, - prompt = "Client replay: ", + prompt = "Client replay", text = self.master.state.last_saveload, callback = self.master.client_playback_path ) @@ -59,7 +59,7 @@ class Window(urwid.Frame): elif k == "i": signals.status_prompt.send( self, - prompt = "Intercept filter: ", + prompt = "Intercept filter", text = self.master.state.intercept_txt, callback = self.master.set_intercept ) @@ -99,9 +99,9 @@ class Window(urwid.Frame): ) elif k == "S": if not self.master.server_playback: - signals.status_path_prompt.send( + signals.status_prompt_path.send( self, - prompt = "Server replay path: ", + prompt = "Server replay path", text = self.master.state.last_saveload, callback = self.master.server_playback_path ) @@ -130,13 +130,13 @@ class Window(urwid.Frame): ) elif k == "t": signals.status_prompt.send( - prompt = "Sticky cookie filter: ", + prompt = "Sticky cookie filter", text = self.master.stickycookie_txt, callback = self.master.set_stickycookie ) elif k == "u": signals.status_prompt.send( - prompt = "Sticky auth filter: ", + prompt = "Sticky auth filter", text = self.master.stickyauth_txt, callback = self.master.set_stickyauth ) -- cgit v1.2.3 From 200498e7aa57effd7158c8d735f95c6556203a07 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 14:14:44 +1300 Subject: Simplify the way in which path prompts keep state In the past, we kept the last path the user specified for a number of different path types to pre-seed the path prompt. Now, we no longer distinguish between types, and pre-seed with the last used directory regardless. --- libmproxy/console/__init__.py | 6 ------ libmproxy/console/common.py | 2 -- libmproxy/console/flowlist.py | 6 ------ libmproxy/console/flowview.py | 2 -- libmproxy/console/grideditor.py | 2 -- libmproxy/console/statusbar.py | 22 ++++++++++++++++------ libmproxy/console/window.py | 2 -- 7 files changed, 16 insertions(+), 26 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index d8eb8a41..34abe6f4 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -31,8 +31,6 @@ class ConsoleState(flow.State): self.view_mode = common.VIEW_LIST self.view_flow_mode = common.VIEW_FLOW_REQUEST - self.last_script = "" - self.last_saveload = "" self.flowsettings = weakref.WeakKeyDictionary() def add_flow_setting(self, flow, key, value): @@ -258,7 +256,6 @@ class ConsoleMaster(flow.FlowMaster): self._run_script_method("error", s, f) s.unload() self.refresh_flow(f) - self.state.last_script = command def set_script(self, command): if not command: @@ -266,7 +263,6 @@ class ConsoleMaster(flow.FlowMaster): ret = self.load_script(command) if ret: signals.status_message.send(message=ret) - self.state.last_script = command def toggle_eventlog(self): self.eventlog = not self.eventlog @@ -501,7 +497,6 @@ class ConsoleMaster(flow.FlowMaster): self.help_context = flowview.help_context def _write_flows(self, path, flows): - self.state.last_saveload = path if not path: return path = os.path.expanduser(path) @@ -527,7 +522,6 @@ class ConsoleMaster(flow.FlowMaster): return ret or "Flows loaded from %s"%path def load_flows_path(self, path): - self.state.last_saveload = path reterr = None try: flow.FlowMaster.load_flows_file(self, path) diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index e4ecde91..c0593af4 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -193,7 +193,6 @@ def raw_format_flow(f, focus, extended, padding): def save_data(path, data, master, state): if not path: return - state.last_saveload = path path = os.path.expanduser(path) try: with file(path, "wb") as f: @@ -205,7 +204,6 @@ def save_data(path, data, master, state): def ask_save_path(prompt, data, master, state): signals.status_prompt_path.send( prompt = prompt, - text = state.last_saveload, callback = save_data, args = (data, master, state) ) diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index f39188bb..946bd97b 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -113,13 +113,11 @@ class ConnectionItem(urwid.WidgetWrap): if k == "a": signals.status_prompt_path.send( prompt = "Save all flows to", - text = self.state.last_saveload, callback = self.master.save_flows ) else: signals.status_prompt_path.send( prompt = "Save this flow to", - text = self.state.last_saveload, callback = self.master.save_one_flow, args = (self.flow,) ) @@ -152,7 +150,6 @@ class ConnectionItem(urwid.WidgetWrap): else: signals.status_prompt_path.send( prompt = "Server replay path", - text = self.state.last_saveload, callback = self.master.server_playback_path ) @@ -218,7 +215,6 @@ class ConnectionItem(urwid.WidgetWrap): elif key == "|": signals.status_prompt_path.send( prompt = "Send flow to script", - text = self.state.last_script, callback = self.master.run_script_once, args = (self.flow,) ) @@ -316,7 +312,6 @@ class FlowListBox(urwid.ListBox): signals.status_prompt_path.send( self, prompt = "Load flows", - text = self.master.state.last_saveload, callback = self.master.load_flows_callback ) elif key == "n": @@ -334,7 +329,6 @@ class FlowListBox(urwid.ListBox): signals.status_prompt_path.send( self, prompt = "Stream flows to", - text = self.master.state.last_saveload, callback = self.master.start_stream_to_path ) else: diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index b9d5fbca..d63b8a8c 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -771,7 +771,6 @@ class FlowView(urwid.WidgetWrap): elif key == "W": signals.status_prompt_path.send( prompt = "Save this flow", - text = self.state.last_saveload, callback = self.master.save_one_flow, args = (self.flow,) ) @@ -786,7 +785,6 @@ class FlowView(urwid.WidgetWrap): elif key == "|": signals.status_prompt_path.send( prompt = "Send flow to script", - text = self.state.last_script, callback = self.master.run_script_once, args = (self.flow,) ) diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index e7c9854b..dc3bad0e 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -341,7 +341,6 @@ class GridEditor(urwid.WidgetWrap): signals.status_prompt_path.send( self, prompt = "Read file", - text = "", callback = self.read_file ) elif key == "R": @@ -349,7 +348,6 @@ class GridEditor(urwid.WidgetWrap): signals.status_prompt_path.send( self, prompt = "Read unescaped file", - text = "", callback = self.read_file, args = (True,) ) diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index 7ff26b15..30819188 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -1,4 +1,5 @@ import time +import os.path import urwid @@ -15,8 +16,12 @@ class ActionBar(urwid.WidgetWrap): signals.status_prompt_path.connect(self.sig_path_prompt) signals.status_prompt_onekey.connect(self.sig_prompt_onekey) + self.last_path = "" + self.prompting = False self.onekey = False + self.pathprompt = False + def sig_message(self, sender, message, expire=None): w = urwid.Text(message) @@ -35,9 +40,13 @@ class ActionBar(urwid.WidgetWrap): self._w = urwid.Edit(self.prep_prompt(prompt), text or "") self.prompting = (callback, args) - def sig_path_prompt(self, sender, prompt, text, callback, args=()): + def sig_path_prompt(self, sender, prompt, callback, args=()): signals.focus.send(self, section="footer") - self._w = pathedit.PathEdit(self.prep_prompt(prompt), text) + self._w = pathedit.PathEdit( + self.prep_prompt(prompt), + os.path.dirname(self.last_path) + ) + self.pathprompt = True self.prompting = (callback, args) def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): @@ -71,7 +80,7 @@ class ActionBar(urwid.WidgetWrap): elif k in self.onekey: self.prompt_execute(k) elif k == "enter": - self.prompt_execute() + self.prompt_execute(self._w.get_edit_text()) else: if common.is_keypress(k): self._w.keypress(size, k) @@ -84,12 +93,13 @@ class ActionBar(urwid.WidgetWrap): def prompt_done(self): self.prompting = False self.onekey = False + self.pathprompt = False signals.status_message.send(message="") signals.focus.send(self, section="body") - def prompt_execute(self, txt=None): - if not txt: - txt = self._w.get_edit_text() + def prompt_execute(self, txt): + if self.pathprompt: + self.last_path = txt p, args = self.prompting self.prompt_done() msg = p(txt, *args) diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index 87f06637..d686f61d 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -19,7 +19,6 @@ class Window(urwid.Frame): signals.status_prompt_path.send( self, prompt = "Client replay", - text = self.master.state.last_saveload, callback = self.master.client_playback_path ) else: @@ -102,7 +101,6 @@ class Window(urwid.Frame): signals.status_prompt_path.send( self, prompt = "Server replay path", - text = self.master.state.last_saveload, callback = self.master.server_playback_path ) else: -- cgit v1.2.3 From 941584623281905fec22d8857c5501d196c051f7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 22 Mar 2015 02:25:47 +0100 Subject: web: raw content view --- libmproxy/web/static/app.css | 3 ++- libmproxy/web/static/app.js | 58 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 7 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/web/static/app.css b/libmproxy/web/static/app.css index 91e847a4..ccaefe92 100644 --- a/libmproxy/web/static/app.css +++ b/libmproxy/web/static/app.css @@ -271,7 +271,8 @@ header .menu { } .flow-detail { width: 100%; - overflow: auto; + overflow-x: auto; + overflow-y: scroll; } .flow-detail nav { background-color: #F2F2F2; diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index eb8ef45e..2254b415 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -1195,23 +1195,56 @@ var Image = React.createClass({displayName: "Image", } }, render: function () { - var message_name = this.props.flow.request === this.props.message ? "request" : "response"; - var url = "/flows/" + this.props.flow.id + "/" + message_name + "/content"; + var url = MessageUtils.getContentURL(this.props.flow, this.props.message); return React.createElement("div", {className: "flowview-image"}, React.createElement("img", {src: url, alt: "preview", className: "img-thumbnail"}) ); } }); +var RawMixin = { + getInitialState: function () { + return { + content: undefined + } + }, + requestContent: function (nextProps) { + this.setState({content: undefined}); + var request = MessageUtils.getContent(nextProps.flow, nextProps.message); + request.done(function (data) { + this.setState({content: data}); + }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) { + this.setState({content: "AJAX Error: " + textStatus}); + }.bind(this)); + + }, + componentWillMount: function () { + this.requestContent(this.props); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.requestContent(nextProps); + } + }, + render: function () { + if (!this.state.content) { + return React.createElement("div", {className: "text-center"}, + React.createElement("i", {className: "fa fa-spinner fa-spin"}) + ); + } + return this.renderContent(); + } +}; + var Raw = React.createClass({displayName: "Raw", + mixins: [RawMixin], statics: { matches: function (message) { return true; } }, - render: function () { - //FIXME - return React.createElement("div", null, "raw"); + renderContent: function () { + return React.createElement("pre", null, this.state.content); } }); @@ -4426,6 +4459,7 @@ module.exports = (function() { },{"../flow/utils.js":21}],21:[function(require,module,exports){ var _ = require("lodash"); +var $ = require("jquery"); var MessageUtils = { getContentType: function (message) { @@ -4461,6 +4495,18 @@ var MessageUtils = { } } return false; + }, + getContentURL: function(flow, message){ + if(message === flow.request){ + message = "request"; + } else if (message === flow.response){ + message = "response"; + } + return "/flows/" + flow.id + "/" + message + "/content"; + }, + getContent: function(flow, message){ + var url = MessageUtils.getContentURL(flow, message); + return $.get(url); } }; @@ -4492,7 +4538,7 @@ module.exports = { MessageUtils: MessageUtils }; -},{"lodash":"lodash"}],22:[function(require,module,exports){ +},{"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){ var _ = require("lodash"); var $ = require("jquery"); -- cgit v1.2.3 From c9a09754464e27a5f34295d8a1c0b435248c104c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 15:11:54 +1300 Subject: console: observe state objects for changes, fire event to update status bar. --- libmproxy/console/__init__.py | 9 +++++++++ libmproxy/console/signals.py | 3 +++ libmproxy/console/statusbar.py | 4 ++++ 3 files changed, 16 insertions(+) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 34abe6f4..b593d282 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -33,6 +33,10 @@ class ConsoleState(flow.State): self.flowsettings = weakref.WeakKeyDictionary() + def __setattr__(self, name, value): + self.__dict__[name] = value + signals.update_settings.send(self) + def add_flow_setting(self, flow, key, value): d = self.flowsettings.setdefault(flow, {}) d[key] = value @@ -212,6 +216,10 @@ class ConsoleMaster(flow.FlowMaster): self.start_app(self.options.app_host, self.options.app_port) signals.call_in.connect(self.sig_call_in) + def __setattr__(self, name, value): + self.__dict__[name] = value + signals.update_settings.send(self) + def sig_call_in(self, sender, seconds, callback, args=()): def cb(*_): return callback(*args) @@ -598,6 +606,7 @@ class ConsoleMaster(flow.FlowMaster): elif a == "u": self.server.config.no_upstream_cert =\ not self.server.config.no_upstream_cert + signals.update_settings.send(self) def shutdown(self): self.state.killall(self) diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py index e8944afb..a62b2a4e 100644 --- a/libmproxy/console/signals.py +++ b/libmproxy/console/signals.py @@ -17,3 +17,6 @@ call_in = blinker.Signal() # Focus the body, footer or header of the main window focus = blinker.Signal() + +# Fired when settings change +update_settings = blinker.Signal() diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index 30819188..7663ee44 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -113,6 +113,10 @@ class StatusBar(urwid.WidgetWrap): self.ab = ActionBar() self.ib = urwid.WidgetWrap(urwid.Text("")) self._w = urwid.Pile([self.ib, self.ab]) + signals.update_settings.connect(self.sig_update_settings) + + def sig_update_settings(self, sender): + self.redraw() def keypress(self, *args, **kwargs): return self.ab.keypress(*args, **kwargs) -- cgit v1.2.3 From aa9a38522f5fbfef556578b6018ad365ad5e844d Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 15:58:32 +1300 Subject: Remove refresh_flow mechanism in favor of a signal-based implementation --- libmproxy/console/__init__.py | 21 +++++---------- libmproxy/console/flowview.py | 60 ++++++++++++++++++++++++------------------- libmproxy/console/signals.py | 3 +++ 3 files changed, 43 insertions(+), 41 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index b593d282..d988ba84 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -263,7 +263,7 @@ class ConsoleMaster(flow.FlowMaster): if f.error: self._run_script_method("error", s, f) s.unload() - self.refresh_flow(f) + signals.flow_change.send(self, flow = f) def set_script(self, command): if not command: @@ -378,7 +378,7 @@ class ConsoleMaster(flow.FlowMaster): changed = self.tick(self.masterq, timeout=0) if changed: self.loop.draw_screen() - self.statusbar.redraw() + signals.update_settings.send() self.loop.set_alarm_in(0.01, self.ticker) def run(self): @@ -397,7 +397,6 @@ class ConsoleMaster(flow.FlowMaster): screen = self.ui, ) self.view_flowlist() - self.statusbar.redraw() self.server.start_slave( controller.Slave, @@ -446,7 +445,6 @@ class ConsoleMaster(flow.FlowMaster): header = self.header, footer = self.statusbar ) - self.statusbar.redraw() return self.view def view_help(self): @@ -633,15 +631,10 @@ class ConsoleMaster(flow.FlowMaster): def refresh_focus(self): if self.state.view: - self.refresh_flow(self.state.view[self.state.focus]) - - def refresh_flow(self, c): - if hasattr(self.header, "refresh_flow"): - self.header.refresh_flow(c) - if hasattr(self.body, "refresh_flow"): - self.body.refresh_flow(c) - if hasattr(self.statusbar, "refresh_flow"): - self.statusbar.refresh_flow(c) + signals.flow_change.send( + self, + flow = self.state.view[self.state.focus] + ) def process_flow(self, f): if self.state.intercept and f.match(self.state.intercept) and not f.request.is_replay: @@ -649,7 +642,7 @@ class ConsoleMaster(flow.FlowMaster): else: f.reply() self.sync_list_view() - self.refresh_flow(f) + signals.flow_change.send(self, flow = f) def clear_events(self): self.eventlist[:] = [] diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index d63b8a8c..2dd2cb82 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -88,10 +88,17 @@ class FlowViewHeader(urwid.WidgetWrap): def __init__(self, master, f): self.master, self.flow = master, f self._w = common.format_flow(f, False, extended=True, padding=0, hostheader=self.master.showhost) - - def refresh_flow(self, f): - if f == self.flow: - self._w = common.format_flow(f, False, extended=True, padding=0, hostheader=self.master.showhost) + signals.flow_change.connect(self.sig_flow_change) + + def sig_flow_change(self, sender, flow): + if flow == self.flow: + self._w = common.format_flow( + flow, + False, + extended=True, + padding=0, + hostheader=self.master.showhost + ) class CallbackCache: @@ -119,6 +126,14 @@ class FlowView(urwid.WidgetWrap): self.view_response() else: self.view_request() + signals.flow_change.connect(self.sig_flow_change) + + def sig_flow_change(self, sender, flow): + if flow == self.flow: + if self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE and self.flow.response: + self.view_response() + else: + self.view_request() def _cached_content_view(self, viewmode, hdrItems, content, limit, is_request): return contentview.get_content_view(viewmode, hdrItems, content, limit, self.master.add_event, is_request) @@ -332,7 +347,7 @@ class FlowView(urwid.WidgetWrap): list_box = urwid.ListBox(merged) list_box.set_focus(focus_position + 2) self._w = self.wrap_body(const, list_box) - self.master.statusbar.redraw() + signals.update_settings.send(self) self.last_displayed_body = list_box @@ -456,7 +471,6 @@ class FlowView(urwid.WidgetWrap): self.state.view_flow_mode = common.VIEW_FLOW_REQUEST body = self.conn_text(self.flow.request) self._w = self.wrap_body(common.VIEW_FLOW_REQUEST, body) - self.master.statusbar.redraw() def view_response(self): self.state.view_flow_mode = common.VIEW_FLOW_RESPONSE @@ -476,19 +490,11 @@ class FlowView(urwid.WidgetWrap): ] ) self._w = self.wrap_body(common.VIEW_FLOW_RESPONSE, body) - self.master.statusbar.redraw() - - def refresh_flow(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 set_method_raw(self, m): if m: self.flow.request.method = m - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def edit_method(self, m): if m == "e": @@ -501,7 +507,7 @@ class FlowView(urwid.WidgetWrap): for i in common.METHOD_OPTIONS: if i[1] == m: self.flow.request.method = i[0].upper() - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_url(self, url): request = self.flow.request @@ -509,7 +515,7 @@ class FlowView(urwid.WidgetWrap): request.url = str(url) except ValueError: return "Invalid URL." - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_resp_code(self, code): response = self.flow.response @@ -520,12 +526,12 @@ class FlowView(urwid.WidgetWrap): import BaseHTTPServer if BaseHTTPServer.BaseHTTPRequestHandler.responses.has_key(int(code)): response.msg = BaseHTTPServer.BaseHTTPRequestHandler.responses[int(code)][0] - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_resp_msg(self, msg): response = self.flow.response response.msg = msg - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def set_headers(self, lst, conn): conn.headers = flow.ODictCaseless(lst) @@ -614,7 +620,7 @@ class FlowView(urwid.WidgetWrap): text = message.msg, callback = self.set_resp_msg ) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def _view_nextprev_flow(self, np, flow): try: @@ -642,7 +648,7 @@ class FlowView(urwid.WidgetWrap): (self.state.view_flow_mode, "prettyview"), contentview.get_by_shortcut(t) ) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def delete_body(self, t): if t == "m": @@ -653,7 +659,7 @@ class FlowView(urwid.WidgetWrap): self.flow.request.content = val else: self.flow.response.content = val - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) def keypress(self, size, key): if key == " ": @@ -736,7 +742,7 @@ class FlowView(urwid.WidgetWrap): (self.state.view_flow_mode, "fullcontents"), True ) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) signals.status_message.send(message="") elif key == "g": if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: @@ -760,13 +766,13 @@ class FlowView(urwid.WidgetWrap): r = self.master.replay_request(self.flow) if r: signals.status_message.send(message=r) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) elif key == "V": if not self.flow.modified(): signals.status_message.send(message="Flow not modified.") return self.state.revert(self.flow) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) signals.status_message.send(message="Reverted.") elif key == "W": signals.status_prompt_path.send( @@ -817,7 +823,7 @@ class FlowView(urwid.WidgetWrap): callback = self.encode_callback, args = (conn,) ) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) elif key == "/": last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") search_prompt = "Search body ["+last_search_string+"]" if last_search_string else "Search body" @@ -839,4 +845,4 @@ class FlowView(urwid.WidgetWrap): "d": "deflate", } conn.encode(encoding_map[key]) - self.master.refresh_flow(self.flow) + signals.flow_change.send(self, flow = self.flow) diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py index a62b2a4e..9afde6f4 100644 --- a/libmproxy/console/signals.py +++ b/libmproxy/console/signals.py @@ -20,3 +20,6 @@ focus = blinker.Signal() # Fired when settings change update_settings = blinker.Signal() + +# Fired when a flow changes +flow_change = blinker.Signal() -- cgit v1.2.3 From 120c8db8a413018bde60d156f480ade001b492ef Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 16:59:11 +1300 Subject: console: refactor the way we keep global view state --- libmproxy/console/__init__.py | 99 ++++++++++++++++++------------------- libmproxy/console/flowdetailview.py | 5 +- libmproxy/console/grideditor.py | 10 ++-- libmproxy/console/help.py | 5 +- libmproxy/console/statusbar.py | 7 ++- 5 files changed, 60 insertions(+), 66 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index d988ba84..f6f8e721 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -188,8 +188,6 @@ class ConsoleMaster(flow.FlowMaster): self.eventlog = options.eventlog self.eventlist = urwid.SimpleListWalker([]) - self.statusbar = None - if options.client_replay: self.client_playback_path(options.client_replay) @@ -287,12 +285,7 @@ class ConsoleMaster(flow.FlowMaster): try: return flow.read_flows_from_paths([path]) except flow.FlowReadError as e: - if not self.statusbar: - print >> sys.stderr, e.strerror - sys.exit(1) - else: - signals.status_message.send(message=e.strerror) - return None + signals.status_message.send(message=e.strerror) def client_playback_path(self, path): flows = self._readflows(path) @@ -326,7 +319,9 @@ class ConsoleMaster(flow.FlowMaster): try: subprocess.call(cmd) except: - signals.status_message.send(message="Can't start editor: %s" % " ".join(c)) + signals.status_message.send( + message = "Can't start editor: %s" % " ".join(c) + ) else: data = open(name, "rb").read() self.ui.start() @@ -386,17 +381,11 @@ class ConsoleMaster(flow.FlowMaster): self.ui.set_terminal_properties(256) self.ui.register_palette(self.palette.palette()) self.flow_list_walker = flowlist.FlowListWalker(self, self.state) - self.view = None - self.statusbar = None - self.header = None - self.body = None self.help_context = None - self.onekey = False self.loop = urwid.MainLoop( - self.view, + urwid.SolidFill("x"), screen = self.ui, ) - self.view_flowlist() self.server.start_slave( controller.Slave, @@ -425,6 +414,11 @@ class ConsoleMaster(flow.FlowMaster): raise urwid.ExitMainLoop signal.signal(signal.SIGINT, exit) + self.loop.set_alarm_in( + 0.0001, + lambda *args: self.view_flowlist() + ) + try: self.loop.run() except Exception: @@ -438,43 +432,38 @@ class ConsoleMaster(flow.FlowMaster): sys.stderr.flush() self.shutdown() - def make_view(self): - self.view = window.Window( - self, - self.body, - header = self.header, - footer = self.statusbar - ) - return self.view - def view_help(self): - h = help.HelpView( + self.loop.widget = window.Window( self, - self.help_context, - (self.statusbar, self.body, self.header) + help.HelpView( + self, + self.help_context, + self.loop.widget, + ), + None, + statusbar.StatusBar(self, help.footer) ) - self.statusbar = statusbar.StatusBar(self, help.footer) - self.body = h - self.header = None - self.loop.widget = self.make_view() def view_flowdetails(self, flow): - h = flowdetailview.FlowDetailsView( + self.loop.widget = window.Window( self, - flow, - (self.statusbar, self.body, self.header) + flowdetailview.FlowDetailsView( + self, + flow, + self.loop.widget + ), + None, + statusbar.StatusBar(self, flowdetailview.footer) ) - self.statusbar = statusbar.StatusBar(self, flowdetailview.footer) - self.body = h - self.header = None - self.loop.widget = self.make_view() def view_grideditor(self, ge): - self.body = ge - self.header = None self.help_context = ge.make_help() - self.statusbar = statusbar.StatusBar(self, grideditor.footer) - self.loop.widget = self.make_view() + self.loop.widget = window.Window( + self, + ge, + None, + statusbar.StatusBar(self, grideditor.FOOTER) + ) def view_flowlist(self): if self.ui.started: @@ -483,24 +472,30 @@ class ConsoleMaster(flow.FlowMaster): self.state.set_focus(self.state.flow_count()) if self.eventlog: - self.body = flowlist.BodyPile(self) + body = flowlist.BodyPile(self) else: - self.body = flowlist.FlowListBox(self) - self.statusbar = statusbar.StatusBar(self, flowlist.footer) - self.header = None + body = flowlist.FlowListBox(self) self.state.view_mode = common.VIEW_LIST - self.loop.widget = self.make_view() self.help_context = flowlist.help_context + self.loop.widget = window.Window( + self, + body, + None, + statusbar.StatusBar(self, flowlist.footer) + ) + self.loop.draw_screen() def view_flow(self, flow): - self.body = flowview.FlowView(self, self.state, flow) - self.header = flowview.FlowViewHeader(self, flow) - self.statusbar = statusbar.StatusBar(self, flowview.footer) self.state.set_focus_flow(flow) self.state.view_mode = common.VIEW_FLOW - self.loop.widget = self.make_view() self.help_context = flowview.help_context + self.loop.widget = window.Window( + self, + flowview.FlowView(self, self.state, flow), + flowview.FlowViewHeader(self, flow), + statusbar.StatusBar(self, flowview.footer) + ) def _write_flows(self, path, flows): if not path: diff --git a/libmproxy/console/flowdetailview.py b/libmproxy/console/flowdetailview.py index f351bff1..15350ea1 100644 --- a/libmproxy/console/flowdetailview.py +++ b/libmproxy/console/flowdetailview.py @@ -18,10 +18,7 @@ class FlowDetailsView(urwid.ListBox): def keypress(self, size, key): key = common.shortcuts(key) if key == "q": - self.master.statusbar = self.state[0] - self.master.body = self.state[1] - self.master.header = self.state[2] - self.master.loop.widget = self.master.make_view() + self.master.loop.widget = self.state return None elif key == "?": key = None diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index dc3bad0e..a1d662c8 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -10,11 +10,11 @@ from .. import utils, filt, script from netlib import http_uastrings -footer = [ +FOOTER = [ ('heading_key', "enter"), ":edit ", ('heading_key', "q"), ":back ", ] -footer_editing = [ +FOOTER_EDITING = [ ('heading_key', "esc"), ":stop editing ", ] @@ -164,12 +164,12 @@ class GridWalker(urwid.ListWalker): self.editing = GridRow( self.focus_col, True, self.editor, self.lst[self.focus] ) - self.editor.master.statusbar.update(footer_editing) + self.editor.master.loop.widget.footer.update(FOOTER_EDITING) self._modified() def stop_edit(self): if self.editing: - self.editor.master.statusbar.update(footer) + self.editor.master.loop.widget.footer.update(FOOTER) self.set_current_value(self.editing.get_edit_value(), False) self.editing = False self._modified() @@ -268,7 +268,7 @@ class GridEditor(urwid.WidgetWrap): self.lb, header = urwid.Pile([title, h]) ) - self.master.statusbar.update("") + self.master.loop.widget.footer.update("") self.show_empty_msg() def show_empty_msg(self): diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 6bb49a92..109a9792 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -180,10 +180,7 @@ class HelpView(urwid.ListBox): def keypress(self, size, key): key = common.shortcuts(key) if key == "q": - self.master.statusbar = self.state[0] - self.master.body = self.state[1] - self.master.header = self.state[2] - self.master.loop.widget = self.master.make_view() + self.master.loop.widget = self.state return None elif key == "?": key = None diff --git a/libmproxy/console/statusbar.py b/libmproxy/console/statusbar.py index 7663ee44..7fb15aa6 100644 --- a/libmproxy/console/statusbar.py +++ b/libmproxy/console/statusbar.py @@ -114,6 +114,7 @@ class StatusBar(urwid.WidgetWrap): self.ib = urwid.WidgetWrap(urwid.Text("")) self._w = urwid.Pile([self.ib, self.ab]) signals.update_settings.connect(self.sig_update_settings) + self.redraw() def sig_update_settings(self, sender): self.redraw() @@ -188,7 +189,11 @@ class StatusBar(urwid.WidgetWrap): if self.master.state.follow_focus: opts.append("following") if self.master.stream_large_bodies: - opts.append("stream:%s" % utils.pretty_size(self.master.stream_large_bodies.max_size)) + opts.append( + "stream:%s" % utils.pretty_size( + self.master.stream_large_bodies.max_size + ) + ) if opts: r.append("[%s]"%(":".join(opts))) -- cgit v1.2.3 From 08bb07653306ed0f84932391732391227ee07ba2 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 17:18:53 +1300 Subject: console: signal-based view stack, unifying mechanisms for help, flow views, etc. --- libmproxy/console/__init__.py | 36 +++++++++++++++++------------------- libmproxy/console/common.py | 3 --- libmproxy/console/flowdetailview.py | 8 ++++---- libmproxy/console/flowview.py | 12 +++++------- libmproxy/console/grideditor.py | 2 +- libmproxy/console/help.py | 7 +++---- libmproxy/console/signals.py | 5 +++++ 7 files changed, 35 insertions(+), 38 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index f6f8e721..90c8bd89 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -28,7 +28,6 @@ class ConsoleState(flow.State): self.follow_focus = None self.default_body_view = contentview.get("Auto") - self.view_mode = common.VIEW_LIST self.view_flow_mode = common.VIEW_FLOW_REQUEST self.flowsettings = weakref.WeakKeyDictionary() @@ -210,9 +209,13 @@ class ConsoleMaster(flow.FlowMaster): print >> sys.stderr, "Stream file error:", err sys.exit(1) + self.view_stack = [] + if options.app: self.start_app(self.options.app_host, self.options.app_port) signals.call_in.connect(self.sig_call_in) + signals.pop_view_state.connect(self.sig_pop_view_state) + signals.push_view_state.connect(self.sig_push_view_state) def __setattr__(self, name, value): self.__dict__[name] = value @@ -223,6 +226,13 @@ class ConsoleMaster(flow.FlowMaster): return callback(*args) self.loop.set_alarm_in(seconds, cb) + def sig_pop_view_state(self, sender): + if self.view_stack: + self.loop.widget = self.view_stack.pop() + + def sig_push_view_state(self, sender): + self.view_stack.append(self.loop.widget) + def start_stream_to_path(self, path, mode="wb"): path = os.path.expanduser(path) try: @@ -433,30 +443,25 @@ class ConsoleMaster(flow.FlowMaster): self.shutdown() def view_help(self): + signals.push_view_state.send(self) self.loop.widget = window.Window( self, - help.HelpView( - self, - self.help_context, - self.loop.widget, - ), + help.HelpView(self.help_context), None, statusbar.StatusBar(self, help.footer) ) def view_flowdetails(self, flow): + signals.push_view_state.send(self) self.loop.widget = window.Window( self, - flowdetailview.FlowDetailsView( - self, - flow, - self.loop.widget - ), + flowdetailview.FlowDetailsView(low), None, statusbar.StatusBar(self, flowdetailview.footer) ) def view_grideditor(self, ge): + signals.push_view_state.send(self) self.help_context = ge.make_help() self.loop.widget = window.Window( self, @@ -475,7 +480,6 @@ class ConsoleMaster(flow.FlowMaster): body = flowlist.BodyPile(self) else: body = flowlist.FlowListBox(self) - self.state.view_mode = common.VIEW_LIST self.help_context = flowlist.help_context self.loop.widget = window.Window( @@ -487,8 +491,8 @@ class ConsoleMaster(flow.FlowMaster): self.loop.draw_screen() def view_flow(self, flow): + signals.push_view_state.send(self) self.state.set_focus_flow(flow) - self.state.view_mode = common.VIEW_FLOW self.help_context = flowview.help_context self.loop.widget = window.Window( self, @@ -548,12 +552,6 @@ class ConsoleMaster(flow.FlowMaster): self.state.default_body_view = v self.refresh_focus() - def pop_view(self): - if self.state.view_mode == common.VIEW_FLOW: - self.view_flow(self.state.view[self.state.focus]) - else: - self.view_flowlist() - def edit_scripts(self, scripts): commands = [x[0] for x in scripts] # remove outer array if commands == [s.command for s in self.scripts]: diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index c0593af4..a0590bb1 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -13,9 +13,6 @@ try: except: pyperclip = False -VIEW_LIST = 0 -VIEW_FLOW = 1 - VIEW_FLOW_REQUEST = 0 VIEW_FLOW_RESPONSE = 1 diff --git a/libmproxy/console/flowdetailview.py b/libmproxy/console/flowdetailview.py index 15350ea1..8bfdae4a 100644 --- a/libmproxy/console/flowdetailview.py +++ b/libmproxy/console/flowdetailview.py @@ -1,6 +1,6 @@ from __future__ import absolute_import import urwid -from . import common +from . import common, signals from .. import utils footer = [ @@ -8,8 +8,8 @@ footer = [ ] class FlowDetailsView(urwid.ListBox): - def __init__(self, master, flow, state): - self.master, self.flow, self.state = master, flow, state + def __init__(self, flow): + self.flow = flow urwid.ListBox.__init__( self, self.flowtext() @@ -18,7 +18,7 @@ class FlowDetailsView(urwid.ListBox): def keypress(self, size, key): key = common.shortcuts(key) if key == "q": - self.master.loop.widget = self.state + signals.pop_view_state.send(self) return None elif key == "?": key = None diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 2dd2cb82..fcb967cc 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -114,9 +114,6 @@ cache = CallbackCache() class FlowView(urwid.WidgetWrap): - REQ = 0 - RESP = 1 - highlight_color = "focusfield" def __init__(self, master, state, flow): @@ -633,8 +630,9 @@ class FlowView(urwid.WidgetWrap): new_flow, new_idx = self.state.get_prev(idx) if new_flow is None: signals.status_message.send(message="No more flows!") - return - self.master.view_flow(new_flow) + else: + signals.pop_view_state.send(self) + self.master.view_flow(new_flow) def view_next_flow(self, flow): return self._view_nextprev_flow("next", flow) @@ -673,8 +671,8 @@ class FlowView(urwid.WidgetWrap): conn = self.flow.response if key == "q": - self.master.view_flowlist() - key = None + signals.pop_view_state.send(self) + return None elif key == "tab": if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: self.view_response() diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index a1d662c8..4bcc0171 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -323,7 +323,7 @@ class GridEditor(urwid.WidgetWrap): if not i[1] and any([x.strip() for x in i[0]]): res.append(i[0]) self.callback(res, *self.cb_args, **self.cb_kwargs) - self.master.pop_view() + signals.pop_view_state.send(self) elif key in ["h", "left"]: self.walker.left() elif key in ["l", "right"]: diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 109a9792..73cd8a50 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import urwid -from . import common +from . import common, signals from .. import filt, version footer = [ @@ -12,8 +12,7 @@ footer = [ class HelpView(urwid.ListBox): - def __init__(self, master, help_context, state): - self.master, self.state = master, state + def __init__(self, help_context): self.help_context = help_context or [] urwid.ListBox.__init__( self, @@ -180,7 +179,7 @@ class HelpView(urwid.ListBox): def keypress(self, size, key): key = common.shortcuts(key) if key == "q": - self.master.loop.widget = self.state + signals.pop_view_state.send(self) return None elif key == "?": key = None diff --git a/libmproxy/console/signals.py b/libmproxy/console/signals.py index 9afde6f4..e4c11f5a 100644 --- a/libmproxy/console/signals.py +++ b/libmproxy/console/signals.py @@ -23,3 +23,8 @@ update_settings = blinker.Signal() # Fired when a flow changes flow_change = blinker.Signal() + + +# Pop and push view state onto a stack +pop_view_state = blinker.Signal() +push_view_state = blinker.Signal() -- cgit v1.2.3 From 15f65d63f633b6b6a540f74006efe542796aa7e4 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 17:28:13 +1300 Subject: Trigger flow change when flow elements are edited --- libmproxy/console/flowview.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index fcb967cc..04440888 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -532,19 +532,28 @@ class FlowView(urwid.WidgetWrap): def set_headers(self, lst, conn): conn.headers = flow.ODictCaseless(lst) + signals.flow_change.send(self, flow = self.flow) def set_query(self, lst, conn): conn.set_query(flow.ODict(lst)) + signals.flow_change.send(self, flow = self.flow) def set_path_components(self, lst, conn): conn.set_path_components([i[0] for i in lst]) + signals.flow_change.send(self, flow = self.flow) def set_form(self, lst, conn): conn.set_form_urlencoded(flow.ODict(lst)) + signals.flow_change.send(self, flow = self.flow) def edit_form(self, conn): self.master.view_grideditor( - grideditor.URLEncodedFormEditor(self.master, conn.get_form_urlencoded().lst, self.set_form, conn) + grideditor.URLEncodedFormEditor( + self.master, + conn.get_form_urlencoded().lst, + self.set_form, + conn + ) ) def edit_form_confirm(self, key, conn): @@ -586,7 +595,14 @@ class FlowView(urwid.WidgetWrap): else: self.edit_form(message) elif part == "h": - self.master.view_grideditor(grideditor.HeaderEditor(self.master, message.headers.lst, self.set_headers, message)) + self.master.view_grideditor( + grideditor.HeaderEditor( + self.master, + message.headers.lst, + self.set_headers, + message + ) + ) elif part == "p": p = message.get_path_components() p = [[i] for i in p] -- cgit v1.2.3 From a2da38cc8339887abef4efa23cc54fa02c981f3f Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 17:33:25 +1300 Subject: Whitespace, indentation, formatting --- libmproxy/console/flowview.py | 128 +++++++++++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 26 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 04440888..e864cf47 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -87,7 +87,13 @@ footer = [ class FlowViewHeader(urwid.WidgetWrap): def __init__(self, master, f): self.master, self.flow = master, f - self._w = common.format_flow(f, False, extended=True, padding=0, hostheader=self.master.showhost) + self._w = common.format_flow( + f, + False, + extended=True, + padding=0, + hostheader=self.master.showhost + ) signals.flow_change.connect(self.sig_flow_change) def sig_flow_change(self, sender, flow): @@ -133,7 +139,14 @@ class FlowView(urwid.WidgetWrap): self.view_request() def _cached_content_view(self, viewmode, hdrItems, content, limit, is_request): - return contentview.get_content_view(viewmode, hdrItems, content, limit, self.master.add_event, is_request) + return contentview.get_content_view( + viewmode, + hdrItems, + content, + limit, + self.master.add_event, + is_request + ) def content_view(self, viewmode, conn): full = self.state.get_flow_setting( @@ -219,7 +232,8 @@ class FlowView(urwid.WidgetWrap): def conn_text(self, conn): """ - Same as conn_text_raw, but returns result wrapped in a listbox ready for usage. + Same as conn_text_raw, but returns result wrapped in a listbox ready for + usage. """ headers, msg, body = self.conn_text_raw(conn) merged = self.conn_text_merge(headers, msg, body) @@ -290,7 +304,9 @@ class FlowView(urwid.WidgetWrap): """ runs the previous search again, forwards or backwards. """ - last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") + last_search_string = self.state.get_flow_setting( + self.flow, "last_search_string" + ) if last_search_string: message = self.search(last_search_string, backwards) if message: @@ -331,7 +347,11 @@ class FlowView(urwid.WidgetWrap): # generate the body, highlight the words and get focus headers, msg, body = self.conn_text_raw(text) try: - body, focus_position = self.search_highlight_text(body, search_string, backwards=backwards) + body, focus_position = self.search_highlight_text( + body, + search_string, + backwards=backwards + ) except SearchError: return "Search not supported in this view." @@ -348,7 +368,11 @@ class FlowView(urwid.WidgetWrap): self.last_displayed_body = list_box - wrapped, wrapped_message = self.search_wrapped_around(last_find_line, last_search_index, backwards) + wrapped, wrapped_message = self.search_wrapped_around( + last_find_line, + last_search_index, + backwards + ) if wrapped: return wrapped_message @@ -356,9 +380,15 @@ class FlowView(urwid.WidgetWrap): def search_get_start(self, search_string): start_line = 0 start_index = 0 - last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") + last_search_string = self.state.get_flow_setting( + self.flow, + "last_search_string" + ) if search_string == last_search_string: - start_line = self.state.get_flow_setting(self.flow, "last_find_line") + start_line = self.state.get_flow_setting( + self.flow, + "last_find_line" + ) start_index = self.state.get_flow_setting(self.flow, "last_search_index") @@ -403,7 +433,10 @@ class FlowView(urwid.WidgetWrap): found = False text_objects = copy.deepcopy(text_objects) - loop_range = self.search_get_range(len(text_objects), start_line, backwards) + loop_range = self.search_get_range( + len(text_objects), + start_line, backwards + ) for i in loop_range: text_object = text_objects[i] @@ -415,10 +448,19 @@ class FlowView(urwid.WidgetWrap): if i != start_line: start_index = 0 - find_index = self.search_find(text, search_string, start_index, backwards) + find_index = self.search_find( + text, + search_string, + start_index, + backwards + ) if find_index != -1: - new_text = self.search_highlight_object(text, find_index, search_string) + new_text = self.search_highlight_object( + text, + find_index, + search_string + ) text_objects[i] = new_text found = True @@ -436,14 +478,26 @@ class FlowView(urwid.WidgetWrap): focus_pos = None else: if not backwards: - self.state.add_flow_setting(self.flow, "last_search_index", 0) - self.state.add_flow_setting(self.flow, "last_find_line", 0) + self.state.add_flow_setting( + self.flow, "last_search_index", 0 + ) + self.state.add_flow_setting( + self.flow, "last_find_line", 0 + ) else: - self.state.add_flow_setting(self.flow, "last_search_index", None) - self.state.add_flow_setting(self.flow, "last_find_line", len(text_objects) - 1) + self.state.add_flow_setting( + self.flow, "last_search_index", None + ) + self.state.add_flow_setting( + self.flow, "last_find_line", len(text_objects) - 1 + ) - text_objects, focus_pos = self.search_highlight_text(text_objects, - search_string, looping=True, backwards=backwards) + text_objects, focus_pos = self.search_highlight_text( + text_objects, + search_string, + looping=True, + backwards=backwards + ) return text_objects, focus_pos @@ -575,10 +629,12 @@ class FlowView(urwid.WidgetWrap): self.flow.backup() if part == "r": with decoded(message): - # Fix an issue caused by some editors when editing a request/response body. - # Many editors make it hard to save a file without a terminating newline on the last - # line. When editing message bodies, this can cause problems. For now, I just - # strip the newlines off the end of the body when we return from an editor. + # Fix an issue caused by some editors when editing a + # request/response body. Many editors make it hard to save a + # file without a terminating newline on the last line. When + # editing message bodies, this can cause problems. For now, I + # just strip the newlines off the end of the body when we return + # from an editor. c = self.master.spawn_editor(message.content or "") message.content = c.rstrip("\n") elif part == "f": @@ -606,9 +662,22 @@ class FlowView(urwid.WidgetWrap): elif part == "p": p = message.get_path_components() p = [[i] for i in p] - self.master.view_grideditor(grideditor.PathEditor(self.master, p, self.set_path_components, message)) + self.master.view_grideditor( + grideditor.PathEditor( + self.master, + p, + self.set_path_components, + message + ) + ) elif part == "q": - self.master.view_grideditor(grideditor.QueryEditor(self.master, message.get_query().lst, self.set_query, message)) + self.master.view_grideditor( + grideditor.QueryEditor( + self.master, + message.get_query().lst, + self.set_query, message + ) + ) elif part == "u" and self.state.view_flow_mode == common.VIEW_FLOW_REQUEST: signals.status_prompt.send( prompt = "URL", @@ -801,7 +870,9 @@ class FlowView(urwid.WidgetWrap): if os.environ.has_key("EDITOR") or os.environ.has_key("PAGER"): self.master.spawn_external_viewer(conn.content, t) else: - signals.status_message.send(message="Error! Set $EDITOR or $PAGER.") + signals.status_message.send( + message = "Error! Set $EDITOR or $PAGER." + ) elif key == "|": signals.status_prompt_path.send( prompt = "Send flow to script", @@ -826,7 +897,9 @@ class FlowView(urwid.WidgetWrap): e = conn.headers.get_first("content-encoding", "identity") if e != "identity": if not conn.decode(): - signals.status_message.send(message="Could not decode - invalid data?") + signals.status_message.send( + message = "Could not decode - invalid data?" + ) else: signals.status_prompt_onekey.send( prompt = "Select encoding: ", @@ -839,7 +912,10 @@ class FlowView(urwid.WidgetWrap): ) signals.flow_change.send(self, flow = self.flow) elif key == "/": - last_search_string = self.state.get_flow_setting(self.flow, "last_search_string") + last_search_string = self.state.get_flow_setting( + self.flow, + "last_search_string" + ) search_prompt = "Search body ["+last_search_string+"]" if last_search_string else "Search body" signals.status_prompt.send( prompt = search_prompt, -- cgit v1.2.3 From 842e23d3e386169d9a90cef2a634c55a3e5fdd8e Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 21:00:41 +1300 Subject: Replace far-too-clever decorator LRU cache with something simpler --- libmproxy/console/common.py | 9 +++----- libmproxy/console/flowview.py | 15 +++--------- libmproxy/utils.py | 53 +++++++++++++++++++------------------------ 3 files changed, 29 insertions(+), 48 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index a0590bb1..2f143f01 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -327,11 +327,7 @@ def ask_save_body(part, master, state, flow): signals.status_message.send(message="No content to save.") -class FlowCache: - @utils.LRUCache(200) - def format_flow(self, *args): - return raw_format_flow(*args) -flowcache = FlowCache() +flowcache = utils.LRUCache(800) def format_flow(f, focus, extended=False, hostheader=False, padding=2): @@ -370,6 +366,7 @@ def format_flow(f, focus, extended=False, hostheader=False, padding=2): d["resp_ctype"] = t[0].split(";")[0] else: d["resp_ctype"] = "" - return flowcache.format_flow( + return flowcache.get( + raw_format_flow, tuple(sorted(d.items())), focus, extended, padding ) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index e864cf47..2c847fba 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -107,16 +107,7 @@ class FlowViewHeader(urwid.WidgetWrap): ) -class CallbackCache: - @utils.LRUCache(200) - def _callback(self, method, *args, **kwargs): - return getattr(self.obj, method)(*args, **kwargs) - - def callback(self, obj, method, *args, **kwargs): - # obj varies! - self.obj = obj - return self._callback(method, *args, **kwargs) -cache = CallbackCache() +cache = utils.LRUCache(200) class FlowView(urwid.WidgetWrap): @@ -158,8 +149,8 @@ class FlowView(urwid.WidgetWrap): limit = sys.maxint else: limit = contentview.VIEW_CUTOFF - description, text_objects = cache.callback( - self, "_cached_content_view", + description, text_objects = cache.get( + self._cached_content_view, viewmode, tuple(tuple(i) for i in conn.headers.lst), conn.content, diff --git a/libmproxy/utils.py b/libmproxy/utils.py index 51f2dc26..5ed70a45 100644 --- a/libmproxy/utils.py +++ b/libmproxy/utils.py @@ -119,40 +119,33 @@ pkg_data = Data(__name__) class LRUCache: """ - A decorator that implements a self-expiring LRU cache for class - methods (not functions!). - - Cache data is tracked as attributes on the object itself. There is - therefore a separate cache for each object instance. + A simple LRU cache for generated values. """ def __init__(self, size=100): self.size = size + self.cache = {} + self.cacheList = [] + + def get(self, gen, *args): + """ + gen: A (presumably expensive) generator function. The identity of + gen is NOT taken into account by the cache. + *args: A list of immutable arguments, used to establish identiy by + *the cache, and passed to gen to generate values. + """ + if self.cache.has_key(args): + self.cacheList.remove(args) + self.cacheList.insert(0, args) + return self.cache[args] + else: + ret = gen(*args) + self.cacheList.insert(0, args) + self.cache[args] = ret + if len(self.cacheList) > self.size: + d = self.cacheList.pop() + self.cache.pop(d) + return ret - def __call__(self, f): - cacheName = "_cached_%s"%f.__name__ - cacheListName = "_cachelist_%s"%f.__name__ - size = self.size - - @functools.wraps(f) - def wrap(self, *args): - if not hasattr(self, cacheName): - setattr(self, cacheName, {}) - setattr(self, cacheListName, []) - cache = getattr(self, cacheName) - cacheList = getattr(self, cacheListName) - if cache.has_key(args): - cacheList.remove(args) - cacheList.insert(0, args) - return cache[args] - else: - ret = f(self, *args) - cacheList.insert(0, args) - cache[args] = ret - if len(cacheList) > size: - d = cacheList.pop() - cache.pop(d) - return ret - return wrap def parse_content_type(c): """ -- cgit v1.2.3 From 6fb661dab518c036e9333d360f2efc91bc2631ab Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 22 Mar 2015 21:08:18 +1300 Subject: Unwind twisty maze of cache layers. Holy confusing, Batman. --- libmproxy/console/flowview.py | 57 +++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 35 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 2c847fba..1aebb0f0 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -129,43 +129,30 @@ class FlowView(urwid.WidgetWrap): else: self.view_request() - def _cached_content_view(self, viewmode, hdrItems, content, limit, is_request): - return contentview.get_content_view( - viewmode, - hdrItems, - content, - limit, - self.master.add_event, - is_request - ) - def content_view(self, viewmode, conn): - full = self.state.get_flow_setting( - self.flow, - (self.state.view_flow_mode, "fullcontents"), - False - ) - if full: - limit = sys.maxint + if conn.content == CONTENT_MISSING: + msg, body = "", [urwid.Text([("error", "[content missing]")])] + return (msg, body) else: - limit = contentview.VIEW_CUTOFF - description, text_objects = cache.get( - self._cached_content_view, - viewmode, - tuple(tuple(i) for i in conn.headers.lst), - conn.content, - limit, - isinstance(conn, HTTPRequest) - ) - return (description, text_objects) - - def cont_view_handle_missing(self, conn, viewmode): - if conn.content == CONTENT_MISSING: - msg, body = "", [urwid.Text([("error", "[content missing]")])] + full = self.state.get_flow_setting( + self.flow, + (self.state.view_flow_mode, "fullcontents"), + False + ) + if full: + limit = sys.maxint else: - msg, body = self.content_view(viewmode, conn) - - return (msg, body) + limit = contentview.VIEW_CUTOFF + description, text_objects = cache.get( + contentview.get_content_view, + viewmode, + tuple(tuple(i) for i in conn.headers.lst), + conn.content, + limit, + self.master.add_event, + isinstance(conn, HTTPRequest) + ) + return (description, text_objects) def viewmode_get(self, override): return self.state.default_body_view if override is None else override @@ -186,7 +173,7 @@ class FlowView(urwid.WidgetWrap): ) override = self.override_get() viewmode = self.viewmode_get(override) - msg, body = self.cont_view_handle_missing(conn, viewmode) + msg, body = self.content_view(viewmode, conn) return headers, msg, body def conn_text_merge(self, headers, msg, body): -- cgit v1.2.3 From cf9f91b0b4abe2020c544981d6dc2e2e85f4b4bd Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 22 Mar 2015 14:33:42 +0100 Subject: web: upgrade to react 0.13 --- libmproxy/web/static/app.js | 44 +- libmproxy/web/static/vendor.css | 46 +- libmproxy/web/static/vendor.js | 11077 ++++++++++++++++++++++---------------- 3 files changed, 6529 insertions(+), 4638 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index 2254b415..4f3998a9 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -478,31 +478,43 @@ var StickyHeadMixin = { var Navigation = _.extend({}, ReactRouter.Navigation, { setQuery: function (dict) { - var q = this.context.getCurrentQuery(); + var q = this.context.router.getCurrentQuery(); for(var i in dict){ if(dict.hasOwnProperty(i)){ q[i] = dict[i] || undefined; //falsey values shall be removed. } } q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/957 - this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q); + this.replaceWith(this.context.router.getCurrentPath(), this.context.router.getCurrentParams(), q); }, replaceWith: function(routeNameOrPath, params, query) { if(routeNameOrPath === undefined){ - routeNameOrPath = this.context.getCurrentPath(); + routeNameOrPath = this.context.router.getCurrentPath(); } if(params === undefined){ - params = this.context.getCurrentParams(); + params = this.context.router.getCurrentParams(); } if(query === undefined) { - query = this.context.getCurrentQuery(); + query = this.context.router.getCurrentQuery(); } - // FIXME: react-router is just broken. - ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query); + // FIXME: react-router is just broken, + // we hopefully just need to wait for the next release with https://github.com/rackt/react-router/pull/957. + this.context.router.replaceWith(routeNameOrPath, params, query); + } +}); + +// react-router is fairly good at changing its API regularly. +// We keep the old method for now - if it should turn out that their changes are permanent, +// we may remove this mixin and access react-router directly again. +var State = _.extend({}, ReactRouter.State, { + getQuery: function(){ + return this.context.router.getCurrentQuery(); + }, + getParams: function(){ + return this.context.router.getCurrentParams(); } }); -_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes); var Splitter = React.createClass({displayName: "Splitter", getDefaultProps: function () { @@ -610,7 +622,7 @@ var Splitter = React.createClass({displayName: "Splitter", }); module.exports = { - State: ReactRouter.State, // keep here - react-router is pretty buggy, we may need workarounds in the future. + State: State, Navigation: Navigation, StickyHeadMixin: StickyHeadMixin, AutoScrollMixin: AutoScrollMixin, @@ -2166,15 +2178,17 @@ var Header = React.createClass({displayName: "Header", }, render: function () { var header = header_entries.map(function (entry, i) { - var classes = React.addons.classSet({ - active: entry == this.state.active - }); + var className; + if(entry === this.state.active){ + className = "active"; + } else { + className = ""; + } return ( React.createElement("a", {key: i, href: "#", - className: classes, - onClick: this.handleClick.bind(this, entry) - }, + className: className, + onClick: this.handleClick.bind(this, entry)}, entry.title ) ); diff --git a/libmproxy/web/static/vendor.css b/libmproxy/web/static/vendor.css index 149372c8..a170c49a 100644 --- a/libmproxy/web/static/vendor.css +++ b/libmproxy/web/static/vendor.css @@ -945,12 +945,24 @@ th { .glyphicon-bitcoin:before { content: "\e227"; } +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} .glyphicon-yen:before { content: "\00a5"; } +.glyphicon-jpy:before { + content: "\00a5"; +} .glyphicon-ruble:before { content: "\20bd"; } +.glyphicon-rub:before { + content: "\20bd"; +} .glyphicon-scale:before { content: "\e230"; } @@ -1147,6 +1159,9 @@ hr { overflow: visible; clip: auto; } +[role="button"] { + cursor: pointer; +} h1, h2, h3, @@ -2548,10 +2563,13 @@ output { .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { - cursor: not-allowed; background-color: #eeeeee; opacity: 1; } +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} textarea.form-control { height: auto; } @@ -2618,6 +2636,7 @@ input[type="search"] { } .radio-inline, .checkbox-inline { + position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; @@ -2654,6 +2673,7 @@ fieldset[disabled] .checkbox label { padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; + min-height: 34px; } .form-control-static.input-lg, .form-control-static.input-sm { @@ -2695,6 +2715,7 @@ select[multiple].form-group-sm .form-control { padding: 5px 10px; font-size: 12px; line-height: 1.5; + min-height: 32px; } .input-lg { height: 46px; @@ -2731,6 +2752,7 @@ select[multiple].form-group-lg .form-control { padding: 10px 16px; font-size: 18px; line-height: 1.3333333; + min-height: 38px; } .has-feedback { position: relative; @@ -3348,11 +3370,9 @@ input[type="button"].btn-block { } .collapse { display: none; - visibility: hidden; } .collapse.in { display: block; - visibility: visible; } tr.collapse.in { display: table-row; @@ -3377,7 +3397,7 @@ tbody.collapse.in { height: 0; margin-left: 2px; vertical-align: middle; - border-top: 4px solid; + border-top: 4px dashed; border-right: 4px solid transparent; border-left: 4px solid transparent; } @@ -4016,11 +4036,9 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .tab-content > .tab-pane { display: none; - visibility: hidden; } .tab-content > .active { display: block; - visibility: visible; } .nav-tabs .dropdown-menu { margin-top: -1px; @@ -4062,7 +4080,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .navbar-collapse.collapse { display: block !important; - visibility: visible !important; height: auto !important; padding-bottom: 0; overflow: visible !important; @@ -4791,7 +4808,8 @@ a.label:focus { position: relative; top: -1px; } -.btn-xs .badge { +.btn-xs .badge, +.btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } @@ -5614,10 +5632,10 @@ a.list-group-item-danger.active:focus { width: 100%; border: 0; } -.embed-responsive.embed-responsive-16by9 { +.embed-responsive-16by9 { padding-bottom: 56.25%; } -.embed-responsive.embed-responsive-4by3 { +.embed-responsive-4by3 { padding-bottom: 75%; } .well { @@ -5678,7 +5696,7 @@ button.close { right: 0; bottom: 0; left: 0; - z-index: 1040; + z-index: 1050; -webkit-overflow-scrolling: touch; outline: 0; } @@ -5719,10 +5737,12 @@ button.close { outline: 0; } .modal-backdrop { - position: absolute; + position: fixed; top: 0; right: 0; + bottom: 0; left: 0; + z-index: 1040; background-color: #000000; } .modal-backdrop.fade { @@ -5793,7 +5813,6 @@ button.close { position: absolute; z-index: 1070; display: block; - visibility: visible; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; font-weight: normal; @@ -6316,7 +6335,6 @@ button.close { } .hidden { display: none !important; - visibility: hidden !important; } .affix { position: fixed; diff --git a/libmproxy/web/static/vendor.js b/libmproxy/web/static/vendor.js index d98e50d9..6b34edb9 100644 --- a/libmproxy/web/static/vendor.js +++ b/libmproxy/web/static/vendor.js @@ -394,49 +394,432 @@ var invariant = function(condition, format, a, b, c, d, e, f) { module.exports = invariant; },{}],4:[function(require,module,exports){ -"use strict"; +module.exports = require('./lib/'); -/** - * Represents a cancellation caused by navigating away - * before the previous transition has fully resolved. - */ -function Cancellation() {} +},{"./lib/":5}],5:[function(require,module,exports){ +// Load modules -module.exports = Cancellation; -},{}],5:[function(require,module,exports){ -"use strict"; +var Stringify = require('./stringify'); +var Parse = require('./parse'); -var warning = require("react/lib/warning"); -var invariant = require("react/lib/invariant"); -function checkPropTypes(componentName, propTypes, props) { - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error = propTypes[propName](props, propName, componentName); +// Declare internals - if (error instanceof Error) warning(false, error.message); +var internals = {}; + + +module.exports = { + stringify: Stringify, + parse: Parse +}; + +},{"./parse":6,"./stringify":7}],6:[function(require,module,exports){ +// Load modules + +var Utils = require('./utils'); + + +// Declare internals + +var internals = { + delimiter: '&', + depth: 5, + arrayLimit: 20, + parameterLimit: 1000 +}; + + +internals.parseValues = function (str, options) { + + var obj = {}; + var parts = str.split(options.delimiter, options.parameterLimit === Infinity ? undefined : options.parameterLimit); + + for (var i = 0, il = parts.length; i < il; ++i) { + var part = parts[i]; + var pos = part.indexOf(']=') === -1 ? part.indexOf('=') : part.indexOf(']=') + 1; + + if (pos === -1) { + obj[Utils.decode(part)] = ''; + } + else { + var key = Utils.decode(part.slice(0, pos)); + var val = Utils.decode(part.slice(pos + 1)); + + if (Object.prototype.hasOwnProperty(key)) { + continue; + } + + if (!obj.hasOwnProperty(key)) { + obj[key] = val; + } + else { + obj[key] = [].concat(obj[key]).concat(val); + } + } } - } -} -var Configuration = { + return obj; +}; + - statics: { +internals.parseObject = function (chain, val, options) { - validateProps: function validateProps(props) { - checkPropTypes(this.displayName, this.propTypes, props); + if (!chain.length) { + return val; } - }, + var root = chain.shift(); - render: function render() { - invariant(false, "%s elements are for router configuration only and should not be rendered", this.constructor.displayName); - } + var obj = {}; + if (root === '[]') { + obj = []; + obj = obj.concat(internals.parseObject(chain, val, options)); + } + else { + var cleanRoot = root[0] === '[' && root[root.length - 1] === ']' ? root.slice(1, root.length - 1) : root; + var index = parseInt(cleanRoot, 10); + var indexString = '' + index; + if (!isNaN(index) && + root !== cleanRoot && + indexString === cleanRoot && + index >= 0 && + index <= options.arrayLimit) { + + obj = []; + obj[index] = internals.parseObject(chain, val, options); + } + else { + obj[cleanRoot] = internals.parseObject(chain, val, options); + } + } + + return obj; +}; + + +internals.parseKeys = function (key, val, options) { + + if (!key) { + return; + } + + // The regex chunks + + var parent = /^([^\[\]]*)/; + var child = /(\[[^\[\]]*\])/g; + + // Get the parent + + var segment = parent.exec(key); + + // Don't allow them to overwrite object prototype properties + + if (Object.prototype.hasOwnProperty(segment[1])) { + return; + } + + // Stash the parent if it exists + + var keys = []; + if (segment[1]) { + keys.push(segment[1]); + } + + // Loop through children appending to the array until we hit depth + + var i = 0; + while ((segment = child.exec(key)) !== null && i < options.depth) { + + ++i; + if (!Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) { + keys.push(segment[1]); + } + } + + // If there's a remainder, just add whatever is left + + if (segment) { + keys.push('[' + key.slice(segment.index) + ']'); + } + + return internals.parseObject(keys, val, options); +}; + + +module.exports = function (str, options) { + + if (str === '' || + str === null || + typeof str === 'undefined') { + + return {}; + } + + options = options || {}; + options.delimiter = typeof options.delimiter === 'string' || Utils.isRegExp(options.delimiter) ? options.delimiter : internals.delimiter; + options.depth = typeof options.depth === 'number' ? options.depth : internals.depth; + options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : internals.arrayLimit; + options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : internals.parameterLimit; + + var tempObj = typeof str === 'string' ? internals.parseValues(str, options) : str; + var obj = {}; + + // Iterate over the keys and setup the new object + + var keys = Object.keys(tempObj); + for (var i = 0, il = keys.length; i < il; ++i) { + var key = keys[i]; + var newObj = internals.parseKeys(key, tempObj[key], options); + obj = Utils.merge(obj, newObj); + } + + return Utils.compact(obj); +}; + +},{"./utils":8}],7:[function(require,module,exports){ +// Load modules + +var Utils = require('./utils'); + + +// Declare internals + +var internals = { + delimiter: '&', + arrayPrefixGenerators: { + brackets: function (prefix, key) { + return prefix + '[]'; + }, + indices: function (prefix, key) { + return prefix + '[' + key + ']'; + }, + repeat: function (prefix, key) { + return prefix; + } + } +}; + + +internals.stringify = function (obj, prefix, generateArrayPrefix) { + + if (Utils.isBuffer(obj)) { + obj = obj.toString(); + } + else if (obj instanceof Date) { + obj = obj.toISOString(); + } + else if (obj === null) { + obj = ''; + } + + if (typeof obj === 'string' || + typeof obj === 'number' || + typeof obj === 'boolean') { + + return [encodeURIComponent(prefix) + '=' + encodeURIComponent(obj)]; + } + + var values = []; + + if (typeof obj === 'undefined') { + return values; + } + + var objKeys = Object.keys(obj); + for (var i = 0, il = objKeys.length; i < il; ++i) { + var key = objKeys[i]; + if (Array.isArray(obj)) { + values = values.concat(internals.stringify(obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix)); + } + else { + values = values.concat(internals.stringify(obj[key], prefix + '[' + key + ']', generateArrayPrefix)); + } + } + + return values; +}; + + +module.exports = function (obj, options) { + + options = options || {}; + var delimiter = typeof options.delimiter === 'undefined' ? internals.delimiter : options.delimiter; + + var keys = []; + + if (typeof obj !== 'object' || + obj === null) { + + return ''; + } + + var arrayFormat; + if (options.arrayFormat in internals.arrayPrefixGenerators) { + arrayFormat = options.arrayFormat; + } + else if ('indices' in options) { + arrayFormat = options.indices ? 'indices' : 'repeat'; + } + else { + arrayFormat = 'indices'; + } + + var generateArrayPrefix = internals.arrayPrefixGenerators[arrayFormat]; + + var objKeys = Object.keys(obj); + for (var i = 0, il = objKeys.length; i < il; ++i) { + var key = objKeys[i]; + keys = keys.concat(internals.stringify(obj[key], key, generateArrayPrefix)); + } + + return keys.join(delimiter); +}; + +},{"./utils":8}],8:[function(require,module,exports){ +// Load modules + + +// Declare internals + +var internals = {}; + + +exports.arrayToObject = function (source) { + + var obj = {}; + for (var i = 0, il = source.length; i < il; ++i) { + if (typeof source[i] !== 'undefined') { + + obj[i] = source[i]; + } + } + + return obj; +}; + + +exports.merge = function (target, source) { + + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (Array.isArray(target)) { + target.push(source); + } + else { + target[source] = true; + } + + return target; + } + + if (typeof target !== 'object') { + target = [target].concat(source); + return target; + } + + if (Array.isArray(target) && + !Array.isArray(source)) { + + target = exports.arrayToObject(target); + } + + var keys = Object.keys(source); + for (var k = 0, kl = keys.length; k < kl; ++k) { + var key = keys[k]; + var value = source[key]; + + if (!target[key]) { + target[key] = value; + } + else { + target[key] = exports.merge(target[key], value); + } + } + + return target; +}; + + +exports.decode = function (str) { + + try { + return decodeURIComponent(str.replace(/\+/g, ' ')); + } catch (e) { + return str; + } +}; + + +exports.compact = function (obj, refs) { + + if (typeof obj !== 'object' || + obj === null) { + + return obj; + } + + refs = refs || []; + var lookup = refs.indexOf(obj); + if (lookup !== -1) { + return refs[lookup]; + } + + refs.push(obj); + + if (Array.isArray(obj)) { + var compacted = []; + + for (var i = 0, il = obj.length; i < il; ++i) { + if (typeof obj[i] !== 'undefined') { + compacted.push(obj[i]); + } + } + + return compacted; + } + + var keys = Object.keys(obj); + for (i = 0, il = keys.length; i < il; ++i) { + var key = keys[i]; + obj[key] = exports.compact(obj[key], refs); + } + + return obj; +}; + + +exports.isRegExp = function (obj) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +}; + + +exports.isBuffer = function (obj) { + if (obj === null || + typeof obj === 'undefined') { + + return false; + } + + return !!(obj.constructor && + obj.constructor.isBuffer && + obj.constructor.isBuffer(obj)); }; -module.exports = Configuration; -},{"react/lib/invariant":182,"react/lib/warning":202}],6:[function(require,module,exports){ +},{}],9:[function(require,module,exports){ +"use strict"; + +/** + * Represents a cancellation caused by navigating away + * before the previous transition has fully resolved. + */ +function Cancellation() {} + +module.exports = Cancellation; +},{}],10:[function(require,module,exports){ "use strict"; var invariant = require("react/lib/invariant"); @@ -467,10 +850,10 @@ var History = { }; module.exports = History; -},{"react/lib/ExecutionEnvironment":64,"react/lib/invariant":182}],7:[function(require,module,exports){ +},{"react/lib/ExecutionEnvironment":60,"react/lib/invariant":189}],11:[function(require,module,exports){ "use strict"; -var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; @@ -520,7 +903,7 @@ var Match = (function () { this.routes = routes; } - _prototypeProperties(Match, { + _createClass(Match, null, { findMatch: { /** @@ -537,9 +920,7 @@ var Match = (function () { for (var i = 0, len = routes.length; match == null && i < len; ++i) match = deepSearch(routes[i], pathname, query); return match; - }, - writable: true, - configurable: true + } } }); @@ -547,11 +928,20 @@ var Match = (function () { })(); module.exports = Match; -},{"./PathUtils":10}],8:[function(require,module,exports){ +},{"./PathUtils":13}],12:[function(require,module,exports){ "use strict"; +var warning = require("react/lib/warning"); var PropTypes = require("./PropTypes"); +function deprecatedMethod(routerMethodName, fn) { + return function () { + warning(false, "Router.Navigation is deprecated. Please use this.context.router." + routerMethodName + "() instead"); + + return fn.apply(this, arguments); + }; +} + /** * A mixin for components that modify the URL. * @@ -559,11 +949,11 @@ var PropTypes = require("./PropTypes"); * * var MyLink = React.createClass({ * mixins: [ Router.Navigation ], - * handleClick: function (event) { + * handleClick(event) { * event.preventDefault(); * this.transitionTo('aRoute', { the: 'params' }, { the: 'query' }); * }, - * render: function () { + * render() { * return ( * Click me! * ); @@ -573,87 +963,52 @@ var PropTypes = require("./PropTypes"); var Navigation = { contextTypes: { - makePath: PropTypes.func.isRequired, - makeHref: PropTypes.func.isRequired, - transitionTo: PropTypes.func.isRequired, - replaceWith: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired + router: PropTypes.router.isRequired }, /** * Returns an absolute URL path created from the given route * name, URL parameters, and query values. */ - makePath: function makePath(to, params, query) { - return this.context.makePath(to, params, query); - }, + makePath: deprecatedMethod("makePath", function (to, params, query) { + return this.context.router.makePath(to, params, query); + }), /** * Returns a string that may safely be used as the href of a * link to the route with the given name. */ - makeHref: function makeHref(to, params, query) { - return this.context.makeHref(to, params, query); - }, + makeHref: deprecatedMethod("makeHref", function (to, params, query) { + return this.context.router.makeHref(to, params, query); + }), /** * Transitions to the URL specified in the arguments by pushing * a new URL onto the history stack. */ - transitionTo: function transitionTo(to, params, query) { - this.context.transitionTo(to, params, query); - }, + transitionTo: deprecatedMethod("transitionTo", function (to, params, query) { + this.context.router.transitionTo(to, params, query); + }), /** * Transitions to the URL specified in the arguments by replacing * the current URL in the history stack. */ - replaceWith: function replaceWith(to, params, query) { - this.context.replaceWith(to, params, query); - }, + replaceWith: deprecatedMethod("replaceWith", function (to, params, query) { + this.context.router.replaceWith(to, params, query); + }), /** * Transitions to the previous URL. */ - goBack: function goBack() { - return this.context.goBack(); - } + goBack: deprecatedMethod("goBack", function () { + return this.context.router.goBack(); + }) }; module.exports = Navigation; -},{"./PropTypes":11}],9:[function(require,module,exports){ -"use strict"; - -var PropTypes = require("./PropTypes"); - -/** - * Provides the router with context for Router.Navigation. - */ -var NavigationContext = { - - childContextTypes: { - makePath: PropTypes.func.isRequired, - makeHref: PropTypes.func.isRequired, - transitionTo: PropTypes.func.isRequired, - replaceWith: PropTypes.func.isRequired, - goBack: PropTypes.func.isRequired - }, - - getChildContext: function getChildContext() { - return { - makePath: this.constructor.makePath.bind(this.constructor), - makeHref: this.constructor.makeHref.bind(this.constructor), - transitionTo: this.constructor.transitionTo.bind(this.constructor), - replaceWith: this.constructor.replaceWith.bind(this.constructor), - goBack: this.constructor.goBack.bind(this.constructor) - }; - } - -}; - -module.exports = NavigationContext; -},{"./PropTypes":11}],10:[function(require,module,exports){ +},{"./PropTypes":14,"react/lib/warning":210}],13:[function(require,module,exports){ "use strict"; var invariant = require("react/lib/invariant"); @@ -797,7 +1152,7 @@ var PathUtils = { if (existingQuery) query = query ? merge(existingQuery, query) : existingQuery; - var queryString = qs.stringify(query, { indices: false }); + var queryString = qs.stringify(query, { arrayFormat: "brackets" }); if (queryString) { return PathUtils.withoutQuery(path) + "?" + queryString; @@ -807,27 +1162,39 @@ var PathUtils = { }; module.exports = PathUtils; -},{"qs":38,"qs/lib/utils":42,"react/lib/invariant":182}],11:[function(require,module,exports){ +},{"qs":4,"qs/lib/utils":8,"react/lib/invariant":189}],14:[function(require,module,exports){ "use strict"; var assign = require("react/lib/Object.assign"); var ReactPropTypes = require("react").PropTypes; +var Route = require("./Route"); -var PropTypes = assign({ +var PropTypes = assign({}, ReactPropTypes, { /** - * Requires that the value of a prop be falsy. + * Indicates that a prop should be falsy. */ falsy: function falsy(props, propName, componentName) { if (props[propName]) { return new Error("<" + componentName + "> may not have a \"" + propName + "\" prop"); } - } + }, + + /** + * Indicates that a prop should be a Route object. + */ + route: ReactPropTypes.instanceOf(Route), -}, ReactPropTypes); + /** + * Indicates that a prop should be a Router object. + */ + //router: ReactPropTypes.instanceOf(Router) // TODO + router: ReactPropTypes.func + +}); module.exports = PropTypes; -},{"react":"react","react/lib/Object.assign":70}],12:[function(require,module,exports){ +},{"./Route":16,"react":"react","react/lib/Object.assign":67}],15:[function(require,module,exports){ "use strict"; /** @@ -840,10 +1207,10 @@ function Redirect(to, params, query) { } module.exports = Redirect; -},{}],13:[function(require,module,exports){ +},{}],16:[function(require,module,exports){ "use strict"; -var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; @@ -869,7 +1236,33 @@ var Route = (function () { this.handler = handler; } - _prototypeProperties(Route, { + _createClass(Route, { + appendChild: { + + /** + * Appends the given route to this route's child routes. + */ + + value: function appendChild(route) { + invariant(route instanceof Route, "route.appendChild must use a valid Route"); + + if (!this.childRoutes) this.childRoutes = []; + + this.childRoutes.push(route); + } + }, + toString: { + value: function toString() { + var string = ""; + + return string; + } + } + }, { createRoute: { /** @@ -928,7 +1321,7 @@ var Route = (function () { if (path && !(options.isDefault || options.isNotFound)) { if (PathUtils.isAbsolute(path)) { if (parentRoute) { - invariant(parentRoute.paramNames.length === 0, "You cannot nest path \"%s\" inside \"%s\"; the parent requires URL parameters", path, parentRoute.path); + invariant(path === parentRoute.path || parentRoute.paramNames.length === 0, "You cannot nest path \"%s\" inside \"%s\"; the parent requires URL parameters", path, parentRoute.path); } } else if (parentRoute) { // Relative paths extend their parent. @@ -968,9 +1361,7 @@ var Route = (function () { } return route; - }, - writable: true, - configurable: true + } }, createDefaultRoute: { @@ -981,9 +1372,7 @@ var Route = (function () { value: function createDefaultRoute(options) { return Route.createRoute(assign({}, options, { isDefault: true })); - }, - writable: true, - configurable: true + } }, createNotFoundRoute: { @@ -994,9 +1383,7 @@ var Route = (function () { value: function createNotFoundRoute(options) { return Route.createRoute(assign({}, options, { isNotFound: true })); - }, - writable: true, - configurable: true + } }, createRedirect: { @@ -1020,39 +1407,7 @@ var Route = (function () { transition.redirect(options.to, options.params || params, options.query || query); } })); - }, - writable: true, - configurable: true - } - }, { - appendChild: { - - /** - * Appends the given route to this route's child routes. - */ - - value: function appendChild(route) { - invariant(route instanceof Route, "route.appendChild must use a valid Route"); - - if (!this.childRoutes) this.childRoutes = []; - - this.childRoutes.push(route); - }, - writable: true, - configurable: true - }, - toString: { - value: function toString() { - var string = ""; - - return string; - }, - writable: true, - configurable: true + } } }); @@ -1060,67 +1415,12 @@ var Route = (function () { })(); module.exports = Route; -},{"./PathUtils":10,"react/lib/Object.assign":70,"react/lib/invariant":182,"react/lib/warning":202}],14:[function(require,module,exports){ +},{"./PathUtils":13,"react/lib/Object.assign":67,"react/lib/invariant":189,"react/lib/warning":210}],17:[function(require,module,exports){ "use strict"; -var React = require("react"); -var assign = require("react/lib/Object.assign"); -var PropTypes = require("./PropTypes"); - -var REF_NAME = "__routeHandler__"; - -var RouteHandlerMixin = { - - contextTypes: { - getRouteAtDepth: PropTypes.func.isRequired, - setRouteComponentAtDepth: PropTypes.func.isRequired, - routeHandlers: PropTypes.array.isRequired - }, - - childContextTypes: { - routeHandlers: PropTypes.array.isRequired - }, - - getChildContext: function getChildContext() { - return { - routeHandlers: this.context.routeHandlers.concat([this]) - }; - }, - - componentDidMount: function componentDidMount() { - this._updateRouteComponent(this.refs[REF_NAME]); - }, - - componentDidUpdate: function componentDidUpdate() { - this._updateRouteComponent(this.refs[REF_NAME]); - }, - - componentWillUnmount: function componentWillUnmount() { - this._updateRouteComponent(null); - }, - - _updateRouteComponent: function _updateRouteComponent(component) { - this.context.setRouteComponentAtDepth(this.getRouteDepth(), component); - }, - - getRouteDepth: function getRouteDepth() { - return this.context.routeHandlers.length; - }, - - createChildRouteHandler: function createChildRouteHandler(props) { - var route = this.context.getRouteAtDepth(this.getRouteDepth()); - return route ? React.createElement(route.handler, assign({}, props || this.props, { ref: REF_NAME })) : null; - } - -}; - -module.exports = RouteHandlerMixin; -},{"./PropTypes":11,"react":"react","react/lib/Object.assign":70}],15:[function(require,module,exports){ -"use strict"; - -var invariant = require("react/lib/invariant"); -var canUseDOM = require("react/lib/ExecutionEnvironment").canUseDOM; -var getWindowScrollPosition = require("./getWindowScrollPosition"); +var invariant = require("react/lib/invariant"); +var canUseDOM = require("react/lib/ExecutionEnvironment").canUseDOM; +var getWindowScrollPosition = require("./getWindowScrollPosition"); function shouldUpdateScroll(state, prevState) { if (!prevState) { @@ -1191,11 +1491,20 @@ var ScrollHistory = { }; module.exports = ScrollHistory; -},{"./getWindowScrollPosition":30,"react/lib/ExecutionEnvironment":64,"react/lib/invariant":182}],16:[function(require,module,exports){ +},{"./getWindowScrollPosition":32,"react/lib/ExecutionEnvironment":60,"react/lib/invariant":189}],18:[function(require,module,exports){ "use strict"; +var warning = require("react/lib/warning"); var PropTypes = require("./PropTypes"); +function deprecatedMethod(routerMethodName, fn) { + return function () { + warning(false, "Router.State is deprecated. Please use this.context.router." + routerMethodName + "() instead"); + + return fn.apply(this, arguments); + }; +} + /** * A mixin for components that need to know the path, routes, URL * params and query that are currently active. @@ -1204,7 +1513,7 @@ var PropTypes = require("./PropTypes"); * * var AboutLink = React.createClass({ * mixins: [ Router.State ], - * render: function () { + * render() { * var className = this.props.className; * * if (this.isActive('about')) @@ -1217,158 +1526,56 @@ var PropTypes = require("./PropTypes"); var State = { contextTypes: { - getCurrentPath: PropTypes.func.isRequired, - getCurrentRoutes: PropTypes.func.isRequired, - getCurrentPathname: PropTypes.func.isRequired, - getCurrentParams: PropTypes.func.isRequired, - getCurrentQuery: PropTypes.func.isRequired, - isActive: PropTypes.func.isRequired + router: PropTypes.router.isRequired }, /** * Returns the current URL path. */ - getPath: function getPath() { - return this.context.getCurrentPath(); - }, - - /** - * Returns an array of the routes that are currently active. - */ - getRoutes: function getRoutes() { - return this.context.getCurrentRoutes(); - }, + getPath: deprecatedMethod("getCurrentPath", function () { + return this.context.router.getCurrentPath(); + }), /** * Returns the current URL path without the query string. */ - getPathname: function getPathname() { - return this.context.getCurrentPathname(); - }, + getPathname: deprecatedMethod("getCurrentPathname", function () { + return this.context.router.getCurrentPathname(); + }), /** * Returns an object of the URL params that are currently active. */ - getParams: function getParams() { - return this.context.getCurrentParams(); - }, + getParams: deprecatedMethod("getCurrentParams", function () { + return this.context.router.getCurrentParams(); + }), /** * Returns an object of the query params that are currently active. */ - getQuery: function getQuery() { - return this.context.getCurrentQuery(); - }, - - /** - * A helper method to determine if a given route, params, and query - * are active. - */ - isActive: function isActive(to, params, query) { - return this.context.isActive(to, params, query); - } - -}; - -module.exports = State; -},{"./PropTypes":11}],17:[function(require,module,exports){ -"use strict"; - -var assign = require("react/lib/Object.assign"); -var PropTypes = require("./PropTypes"); -var PathUtils = require("./PathUtils"); - -function routeIsActive(activeRoutes, routeName) { - return activeRoutes.some(function (route) { - return route.name === routeName; - }); -} - -function paramsAreActive(activeParams, params) { - for (var property in params) if (String(activeParams[property]) !== String(params[property])) { - return false; - }return true; -} - -function queryIsActive(activeQuery, query) { - for (var property in query) if (String(activeQuery[property]) !== String(query[property])) { - return false; - }return true; -} - -/** - * Provides the router with context for Router.State. - */ -var StateContext = { - - /** - * Returns the current URL path + query string. - */ - getCurrentPath: function getCurrentPath() { - return this.state.path; - }, - - /** - * Returns a read-only array of the currently active routes. - */ - getCurrentRoutes: function getCurrentRoutes() { - return this.state.routes.slice(0); - }, - - /** - * Returns the current URL path without the query string. - */ - getCurrentPathname: function getCurrentPathname() { - return this.state.pathname; - }, + getQuery: deprecatedMethod("getCurrentQuery", function () { + return this.context.router.getCurrentQuery(); + }), /** - * Returns a read-only object of the currently active URL parameters. + * Returns an array of the routes that are currently active. */ - getCurrentParams: function getCurrentParams() { - return assign({}, this.state.params); - }, + getRoutes: deprecatedMethod("getCurrentRoutes", function () { + return this.context.router.getCurrentRoutes(); + }), /** - * Returns a read-only object of the currently active query parameters. - */ - getCurrentQuery: function getCurrentQuery() { - return assign({}, this.state.query); - }, - - /** - * Returns true if the given route, params, and query are active. + * A helper method to determine if a given route, params, and query + * are active. */ - isActive: function isActive(to, params, query) { - if (PathUtils.isAbsolute(to)) { - return to === this.state.path; - }return routeIsActive(this.state.routes, to) && paramsAreActive(this.state.params, params) && (query == null || queryIsActive(this.state.query, query)); - }, - - childContextTypes: { - getCurrentPath: PropTypes.func.isRequired, - getCurrentRoutes: PropTypes.func.isRequired, - getCurrentPathname: PropTypes.func.isRequired, - getCurrentParams: PropTypes.func.isRequired, - getCurrentQuery: PropTypes.func.isRequired, - isActive: PropTypes.func.isRequired - }, - - getChildContext: function getChildContext() { - return { - getCurrentPath: this.getCurrentPath, - getCurrentRoutes: this.getCurrentRoutes, - getCurrentPathname: this.getCurrentPathname, - getCurrentParams: this.getCurrentParams, - getCurrentQuery: this.getCurrentQuery, - isActive: this.isActive - }; - } + isActive: deprecatedMethod("isActive", function (to, params, query) { + return this.context.router.isActive(to, params, query); + }) }; -module.exports = StateContext; -},{"./PathUtils":10,"./PropTypes":11,"react/lib/Object.assign":70}],18:[function(require,module,exports){ +module.exports = State; +},{"./PropTypes":14,"react/lib/warning":210}],19:[function(require,module,exports){ "use strict"; /* jshint -W058 */ @@ -1444,7 +1651,7 @@ Transition.to = function (transition, routes, params, query, callback) { }; module.exports = Transition; -},{"./Cancellation":4,"./Redirect":12}],19:[function(require,module,exports){ +},{"./Cancellation":9,"./Redirect":15}],20:[function(require,module,exports){ "use strict"; /** @@ -1470,7 +1677,7 @@ var LocationActions = { }; module.exports = LocationActions; -},{}],20:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ "use strict"; var LocationActions = require("../actions/LocationActions"); @@ -1500,7 +1707,7 @@ var ImitateBrowserBehavior = { }; module.exports = ImitateBrowserBehavior; -},{"../actions/LocationActions":19}],21:[function(require,module,exports){ +},{"../actions/LocationActions":20}],22:[function(require,module,exports){ "use strict"; /** @@ -1516,12 +1723,56 @@ var ScrollToTopBehavior = { }; module.exports = ScrollToTopBehavior; -},{}],22:[function(require,module,exports){ +},{}],23:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + +/** + * This component is necessary to get around a context warning + * present in React 0.13.0. It sovles this by providing a separation + * between the "owner" and "parent" contexts. + */ + var React = require("react"); -var Configuration = require("../Configuration"); + +var ContextWrapper = (function (_React$Component) { + function ContextWrapper() { + _classCallCheck(this, ContextWrapper); + + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } + + _inherits(ContextWrapper, _React$Component); + + _createClass(ContextWrapper, { + render: { + value: function render() { + return this.props.children; + } + } + }); + + return ContextWrapper; +})(React.Component); + +module.exports = ContextWrapper; +},{"react":"react"}],24:[function(require,module,exports){ +"use strict"; + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var PropTypes = require("../PropTypes"); +var RouteHandler = require("./RouteHandler"); +var Route = require("./Route"); /** * A component is a special kind of that @@ -1529,32 +1780,49 @@ var PropTypes = require("../PropTypes"); * Only one such route may be used at any given level in the * route hierarchy. */ -var DefaultRoute = React.createClass({ - displayName: "DefaultRoute", +var DefaultRoute = (function (_Route) { + function DefaultRoute() { + _classCallCheck(this, DefaultRoute); - mixins: [Configuration], - - propTypes: { - name: PropTypes.string, - path: PropTypes.falsy, - children: PropTypes.falsy, - handler: PropTypes.func.isRequired + if (_Route != null) { + _Route.apply(this, arguments); + } } -}); + _inherits(DefaultRoute, _Route); + + return DefaultRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +DefaultRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +DefaultRoute.defaultProps = { + handler: RouteHandler +}; module.exports = DefaultRoute; -},{"../Configuration":5,"../PropTypes":11,"react":"react"}],23:[function(require,module,exports){ +},{"../PropTypes":14,"./Route":28,"./RouteHandler":29}],25:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var React = require("react"); -var classSet = require("react/lib/cx"); var assign = require("react/lib/Object.assign"); -var Navigation = require("../Navigation"); -var State = require("../State"); var PropTypes = require("../PropTypes"); -var Route = require("../Route"); function isLeftClickEvent(event) { return event.button === 0; @@ -1582,88 +1850,116 @@ function isModifiedEvent(event) { * * */ -var Link = React.createClass({ - displayName: "Link", +var Link = (function (_React$Component) { + function Link() { + _classCallCheck(this, Link); - mixins: [Navigation, State], + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } - propTypes: { - activeClassName: PropTypes.string.isRequired, - to: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Route)]), - params: PropTypes.object, - query: PropTypes.object, - activeStyle: PropTypes.object, - onClick: PropTypes.func - }, + _inherits(Link, _React$Component); - getDefaultProps: function getDefaultProps() { - return { - activeClassName: "active" - }; - }, + _createClass(Link, { + handleClick: { + value: function handleClick(event) { + var allowTransition = true; + var clickResult; - handleClick: function handleClick(event) { - var allowTransition = true; - var clickResult; + if (this.props.onClick) clickResult = this.props.onClick(event); - if (this.props.onClick) clickResult = this.props.onClick(event); + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; - if (isModifiedEvent(event) || !isLeftClickEvent(event)) { - return; - }if (clickResult === false || event.defaultPrevented === true) allowTransition = false; + event.preventDefault(); - event.preventDefault(); + if (allowTransition) this.context.router.transitionTo(this.props.to, this.props.params, this.props.query); + } + }, + getHref: { - if (allowTransition) this.transitionTo(this.props.to, this.props.params, this.props.query); - }, + /** + * Returns the value of the "href" attribute to use on the DOM element. + */ - /** - * Returns the value of the "href" attribute to use on the DOM element. - */ - getHref: function getHref() { - return this.makeHref(this.props.to, this.props.params, this.props.query); - }, + value: function getHref() { + return this.context.router.makeHref(this.props.to, this.props.params, this.props.query); + } + }, + getClassName: { - /** - * Returns the value of the "class" attribute to use on the DOM element, which contains - * the value of the activeClassName property when this is active. - */ - getClassName: function getClassName() { - var classNames = {}; + /** + * Returns the value of the "class" attribute to use on the DOM element, which contains + * the value of the activeClassName property when this is active. + */ - if (this.props.className) classNames[this.props.className] = true; + value: function getClassName() { + var className = this.props.className; - if (this.getActiveState()) classNames[this.props.activeClassName] = true; + if (this.getActiveState()) className += " " + this.props.activeClassName; - return classSet(classNames); - }, + return className; + } + }, + getActiveState: { + value: function getActiveState() { + return this.context.router.isActive(this.props.to, this.props.params, this.props.query); + } + }, + render: { + value: function render() { + var props = assign({}, this.props, { + href: this.getHref(), + className: this.getClassName(), + onClick: this.handleClick.bind(this) + }); - getActiveState: function getActiveState() { - return this.isActive(this.props.to, this.props.params, this.props.query); - }, + if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; - render: function render() { - var props = assign({}, this.props, { - href: this.getHref(), - className: this.getClassName(), - onClick: this.handleClick - }); + return React.DOM.a(props, this.props.children); + } + } + }); - if (props.activeStyle && this.getActiveState()) props.style = props.activeStyle; + return Link; +})(React.Component); - return React.DOM.a(props, this.props.children); - } +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 -}); +Link.contextTypes = { + router: PropTypes.router.isRequired +}; + +Link.propTypes = { + activeClassName: PropTypes.string.isRequired, + to: PropTypes.oneOfType([PropTypes.string, PropTypes.route]).isRequired, + params: PropTypes.object, + query: PropTypes.object, + activeStyle: PropTypes.object, + onClick: PropTypes.func +}; + +Link.defaultProps = { + activeClassName: "active", + className: "" +}; module.exports = Link; -},{"../Navigation":8,"../PropTypes":11,"../Route":13,"../State":16,"react":"react","react/lib/Object.assign":70,"react/lib/cx":160}],24:[function(require,module,exports){ +},{"../PropTypes":14,"react":"react","react/lib/Object.assign":67}],26:[function(require,module,exports){ "use strict"; -var React = require("react"); -var Configuration = require("../Configuration"); +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var PropTypes = require("../PropTypes"); +var RouteHandler = require("./RouteHandler"); +var Route = require("./Route"); /** * A is a special kind of that @@ -1672,56 +1968,95 @@ var PropTypes = require("../PropTypes"); * Only one such route may be used at any given level in the * route hierarchy. */ -var NotFoundRoute = React.createClass({ - - displayName: "NotFoundRoute", - mixins: [Configuration], +var NotFoundRoute = (function (_Route) { + function NotFoundRoute() { + _classCallCheck(this, NotFoundRoute); - propTypes: { - name: PropTypes.string, - path: PropTypes.falsy, - children: PropTypes.falsy, - handler: PropTypes.func.isRequired + if (_Route != null) { + _Route.apply(this, arguments); + } } -}); + _inherits(NotFoundRoute, _Route); + + return NotFoundRoute; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +NotFoundRoute.propTypes = { + name: PropTypes.string, + path: PropTypes.falsy, + children: PropTypes.falsy, + handler: PropTypes.func.isRequired +}; + +NotFoundRoute.defaultProps = { + handler: RouteHandler +}; module.exports = NotFoundRoute; -},{"../Configuration":5,"../PropTypes":11,"react":"react"}],25:[function(require,module,exports){ +},{"../PropTypes":14,"./Route":28,"./RouteHandler":29}],27:[function(require,module,exports){ "use strict"; -var React = require("react"); -var Configuration = require("../Configuration"); +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var PropTypes = require("../PropTypes"); +var Route = require("./Route"); /** * A component is a special kind of that always * redirects to another route when it matches. */ -var Redirect = React.createClass({ - displayName: "Redirect", +var Redirect = (function (_Route) { + function Redirect() { + _classCallCheck(this, Redirect); - mixins: [Configuration], - - propTypes: { - path: PropTypes.string, - from: PropTypes.string, // Alias for path. - to: PropTypes.string, - handler: PropTypes.falsy + if (_Route != null) { + _Route.apply(this, arguments); + } } -}); + _inherits(Redirect, _Route); + + return Redirect; +})(Route); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Redirect.propTypes = { + path: PropTypes.string, + from: PropTypes.string, // Alias for path. + to: PropTypes.string, + handler: PropTypes.falsy +}; + +// Redirects should not have a default handler +Redirect.defaultProps = {}; module.exports = Redirect; -},{"../Configuration":5,"../PropTypes":11,"react":"react"}],26:[function(require,module,exports){ +},{"../PropTypes":14,"./Route":28}],28:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var React = require("react"); -var Configuration = require("../Configuration"); +var invariant = require("react/lib/invariant"); var PropTypes = require("../PropTypes"); var RouteHandler = require("./RouteHandler"); + /** * components specify components that are rendered to the page when the * URL matches a given pattern. @@ -1762,52 +2097,147 @@ var RouteHandler = require("./RouteHandler"); * * If no handler is provided for the route, it will render a matched child route. */ -var Route = React.createClass({ - displayName: "Route", +var Route = (function (_React$Component) { + function Route() { + _classCallCheck(this, Route); - mixins: [Configuration], + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } + } - propTypes: { - name: PropTypes.string, - path: PropTypes.string, - handler: PropTypes.func, - ignoreScrollBehavior: PropTypes.bool - }, + _inherits(Route, _React$Component); - getDefaultProps: function getDefaultProps() { - return { - handler: RouteHandler - }; - } + _createClass(Route, { + render: { + value: function render() { + invariant(false, "%s elements are for router configuration only and should not be rendered", this.constructor.name); + } + } + }); -}); + return Route; +})(React.Component); + +// TODO: Include these in the above class definition +// once we can use ES7 property initializers. +// https://github.com/babel/babel/issues/619 + +Route.propTypes = { + name: PropTypes.string, + path: PropTypes.string, + handler: PropTypes.func, + ignoreScrollBehavior: PropTypes.bool +}; + +Route.defaultProps = { + handler: RouteHandler +}; module.exports = Route; -},{"../Configuration":5,"../PropTypes":11,"./RouteHandler":27,"react":"react"}],27:[function(require,module,exports){ +},{"../PropTypes":14,"./RouteHandler":29,"react":"react","react/lib/invariant":189}],29:[function(require,module,exports){ "use strict"; +var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + +var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var React = require("react"); -var RouteHandlerMixin = require("../RouteHandlerMixin"); +var ContextWrapper = require("./ContextWrapper"); +var assign = require("react/lib/Object.assign"); +var PropTypes = require("../PropTypes"); + +var REF_NAME = "__routeHandler__"; /** * A component renders the active child route handler * when routes are nested. */ -var RouteHandler = React.createClass({ - - displayName: "RouteHandler", - mixins: [RouteHandlerMixin], +var RouteHandler = (function (_React$Component) { + function RouteHandler() { + _classCallCheck(this, RouteHandler); - render: function render() { - return this.createChildRouteHandler(); + if (_React$Component != null) { + _React$Component.apply(this, arguments); + } } -}); + _inherits(RouteHandler, _React$Component); + + _createClass(RouteHandler, { + getChildContext: { + value: function getChildContext() { + return { + routeDepth: this.context.routeDepth + 1 + }; + } + }, + componentDidMount: { + value: function componentDidMount() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, + componentDidUpdate: { + value: function componentDidUpdate() { + this._updateRouteComponent(this.refs[REF_NAME]); + } + }, + componentWillUnmount: { + value: function componentWillUnmount() { + this._updateRouteComponent(null); + } + }, + _updateRouteComponent: { + value: function _updateRouteComponent(component) { + this.context.router.setRouteComponentAtDepth(this.getRouteDepth(), component); + } + }, + getRouteDepth: { + value: function getRouteDepth() { + return this.context.routeDepth; + } + }, + createChildRouteHandler: { + value: function createChildRouteHandler(props) { + var route = this.context.router.getRouteAtDepth(this.getRouteDepth()); + return route ? React.createElement(route.handler, assign({}, props || this.props, { ref: REF_NAME })) : null; + } + }, + render: { + value: function render() { + var handler = this.createChildRouteHandler(); + //