diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-07-25 15:16:16 -0700 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2016-07-25 15:16:16 -0700 |
commit | 79ebcb046e8669f80357a6c3046ec76c6adf49be (patch) | |
tree | 441981a16f1be1e620584e4a47f41767ce5585b2 /mitmproxy | |
parent | 3254595584e1d711e7ae292ad34753a52f7a0fc1 (diff) | |
parent | 56796aeda25dda66621ce78af227ff46049ef811 (diff) | |
download | mitmproxy-79ebcb046e8669f80357a6c3046ec76c6adf49be.tar.gz mitmproxy-79ebcb046e8669f80357a6c3046ec76c6adf49be.tar.bz2 mitmproxy-79ebcb046e8669f80357a6c3046ec76c6adf49be.zip |
Merge remote-tracking branch 'origin/master' into flow_editing_v2
Diffstat (limited to 'mitmproxy')
33 files changed, 442 insertions, 391 deletions
diff --git a/mitmproxy/addons.py b/mitmproxy/addons.py index c779aaf8..a4bea9fa 100644 --- a/mitmproxy/addons.py +++ b/mitmproxy/addons.py @@ -13,16 +13,23 @@ class Addons(object): self.master = master master.options.changed.connect(self.options_update) - def options_update(self, options): + def options_update(self, options, updated): for i in self.chain: with self.master.handlecontext(): - i.configure(options) + i.configure(options, updated) - def add(self, *addons): + def add(self, options, *addons): + if not addons: + raise ValueError("No adons specified.") self.chain.extend(addons) for i in addons: self.invoke_with_context(i, "start") - self.invoke_with_context(i, "configure", self.master.options) + self.invoke_with_context( + i, + "configure", + self.master.options, + self.master.options.keys() + ) def remove(self, addon): self.chain = [i for i in self.chain if i is not addon] diff --git a/mitmproxy/builtins/anticache.py b/mitmproxy/builtins/anticache.py index f208e2fb..41a5ed95 100644 --- a/mitmproxy/builtins/anticache.py +++ b/mitmproxy/builtins/anticache.py @@ -5,7 +5,7 @@ class AntiCache: def __init__(self): self.enabled = False - def configure(self, options): + def configure(self, options, updated): self.enabled = options.anticache def request(self, flow): diff --git a/mitmproxy/builtins/anticomp.py b/mitmproxy/builtins/anticomp.py index 50bd1b73..823e960c 100644 --- a/mitmproxy/builtins/anticomp.py +++ b/mitmproxy/builtins/anticomp.py @@ -5,7 +5,7 @@ class AntiComp: def __init__(self): self.enabled = False - def configure(self, options): + def configure(self, options, updated): self.enabled = options.anticomp def request(self, flow): diff --git a/mitmproxy/builtins/dumper.py b/mitmproxy/builtins/dumper.py index 239630fb..74c2e6b2 100644 --- a/mitmproxy/builtins/dumper.py +++ b/mitmproxy/builtins/dumper.py @@ -5,6 +5,8 @@ import traceback import click +import typing # noqa + from mitmproxy import contentviews from mitmproxy import ctx from mitmproxy import exceptions @@ -19,12 +21,25 @@ def indent(n, text): return "\n".join(pad + i for i in l) -class Dumper(): +class Dumper(object): def __init__(self): - self.filter = None - self.flow_detail = None - self.outfp = None - self.showhost = None + self.filter = None # type: filt.TFilter + self.flow_detail = None # type: int + self.outfp = None # type: typing.io.TextIO + self.showhost = None # type: bool + + def configure(self, options, updated): + if options.filtstr: + self.filter = filt.parse(options.filtstr) + if not self.filter: + raise exceptions.OptionsError( + "Invalid filter expression: %s" % options.filtstr + ) + else: + self.filter = None + self.flow_detail = options.flow_detail + self.outfp = options.tfile + self.showhost = options.showhost def echo(self, text, ident=None, **style): if ident: @@ -59,7 +74,7 @@ class Dumper(): self.echo("") try: - type, lines = contentviews.get_content_view( + _, lines = contentviews.get_content_view( contentviews.get("Auto"), content, headers=getattr(message, "headers", None) @@ -67,7 +82,7 @@ class Dumper(): except exceptions.ContentViewException: s = "Content viewer failed: \n" + traceback.format_exc() ctx.log.debug(s) - type, lines = contentviews.get_content_view( + _, lines = contentviews.get_content_view( contentviews.get("Raw"), content, headers=getattr(message, "headers", None) @@ -114,9 +129,8 @@ class Dumper(): if flow.client_conn: client = click.style( strutils.escape_control_characters( - flow.client_conn.address.host - ), - bold=True + repr(flow.client_conn.address) + ) ) elif flow.request.is_replay: client = click.style("[replay]", fg="yellow", bold=True) @@ -139,17 +153,23 @@ class Dumper(): url = flow.request.url url = click.style(strutils.escape_control_characters(url), bold=True) - httpversion = "" + http_version = "" if flow.request.http_version not in ("HTTP/1.1", "HTTP/1.0"): # We hide "normal" HTTP 1. - httpversion = " " + flow.request.http_version + http_version = " " + flow.request.http_version - line = "{stickycookie}{client} {method} {url}{httpversion}".format( - stickycookie=stickycookie, + if self.flow_detail >= 2: + linebreak = "\n " + else: + linebreak = "" + + line = "{client}: {linebreak}{stickycookie}{method} {url}{http_version}".format( client=client, + stickycookie=stickycookie, + linebreak=linebreak, method=method, url=url, - httpversion=httpversion + http_version=http_version ) self.echo(line) @@ -185,9 +205,14 @@ class Dumper(): size = human.pretty_size(len(flow.response.raw_content)) size = click.style(size, bold=True) - arrows = click.style(" <<", bold=True) + arrows = click.style(" <<", bold=True) + if self.flow_detail == 1: + # This aligns the HTTP response code with the HTTP request method: + # 127.0.0.1:59519: GET http://example.com/ + # << 304 Not Modified 0b + arrows = " " * (len(repr(flow.client_conn.address)) - 2) + arrows - line = "{replay} {arrows} {code} {reason} {size}".format( + line = "{replay}{arrows} {code} {reason} {size}".format( replay=replay, arrows=arrows, code=code, @@ -211,25 +236,12 @@ class Dumper(): def match(self, f): if self.flow_detail == 0: return False - if not self.filt: + if not self.filter: return True - elif f.match(self.filt): + elif f.match(self.filter): return True return False - def configure(self, options): - if options.filtstr: - self.filt = filt.parse(options.filtstr) - if not self.filt: - raise exceptions.OptionsError( - "Invalid filter expression: %s" % options.filtstr - ) - else: - self.filt = None - self.flow_detail = options.flow_detail - self.outfp = options.tfile - self.showhost = options.showhost - def response(self, f): if self.match(f): self.echo_flow(f) @@ -239,8 +251,7 @@ class Dumper(): self.echo_flow(f) def tcp_message(self, f): - # FIXME: Filter should be applied here - if self.options.flow_detail == 0: + if not self.match(f): return message = f.messages[-1] direction = "->" if message.from_client else "<-" diff --git a/mitmproxy/builtins/filestreamer.py b/mitmproxy/builtins/filestreamer.py index 97ddc7c4..ffa565ac 100644 --- a/mitmproxy/builtins/filestreamer.py +++ b/mitmproxy/builtins/filestreamer.py @@ -19,7 +19,7 @@ class FileStreamer: self.stream = io.FilteredFlowWriter(f, filt) self.active_flows = set() - def configure(self, options): + def configure(self, options, updated): # We're already streaming - stop the previous stream and restart if self.stream: self.done() diff --git a/mitmproxy/builtins/replace.py b/mitmproxy/builtins/replace.py index 83b96cee..74d30c05 100644 --- a/mitmproxy/builtins/replace.py +++ b/mitmproxy/builtins/replace.py @@ -8,7 +8,7 @@ class Replace: def __init__(self): self.lst = [] - def configure(self, options): + def configure(self, options, updated): """ .replacements is a list of tuples (fpat, rex, s): diff --git a/mitmproxy/builtins/script.py b/mitmproxy/builtins/script.py index ab068e47..c960dd1c 100644 --- a/mitmproxy/builtins/script.py +++ b/mitmproxy/builtins/script.py @@ -16,6 +16,19 @@ import watchdog.events from watchdog.observers import polling +class NS: + def __init__(self, ns): + self.__dict__["ns"] = ns + + def __getattr__(self, key): + if key not in self.ns: + raise AttributeError("No such element: %s", key) + return self.ns[key] + + def __setattr__(self, key, value): + self.__dict__["ns"][key] = value + + def parse_command(command): """ Returns a (path, args) tuple. @@ -74,18 +87,27 @@ def load_script(path, args): ns = {'__file__': os.path.abspath(path)} with scriptenv(path, args): exec(code, ns, ns) - return ns + return NS(ns) class ReloadHandler(watchdog.events.FileSystemEventHandler): def __init__(self, callback): self.callback = callback + def filter(self, event): + if event.is_directory: + return False + if os.path.basename(event.src_path).startswith("."): + return False + return True + def on_modified(self, event): - self.callback() + if self.filter(event): + self.callback() def on_created(self, event): - self.callback() + if self.filter(event): + self.callback() class Script: @@ -118,29 +140,35 @@ class Script: # It's possible for ns to be un-initialised if we failed during # configure if self.ns is not None and not self.dead: - func = self.ns.get(name) + func = getattr(self.ns, name, None) if func: with scriptenv(self.path, self.args): - func(*args, **kwargs) + return func(*args, **kwargs) def reload(self): self.should_reload.set() + def load_script(self): + self.ns = load_script(self.path, self.args) + ret = self.run("start") + if ret: + self.ns = ret + self.run("start") + def tick(self): if self.should_reload.is_set(): self.should_reload.clear() ctx.log.info("Reloading script: %s" % self.name) self.ns = load_script(self.path, self.args) self.start() - self.configure(self.last_options) + self.configure(self.last_options, self.last_options.keys()) else: self.run("tick") def start(self): - self.ns = load_script(self.path, self.args) - self.run("start") + self.load_script() - def configure(self, options): + def configure(self, options, updated): self.last_options = options if not self.observer: self.observer = polling.PollingObserver() @@ -150,7 +178,7 @@ class Script: os.path.dirname(self.path) or "." ) self.observer.start() - self.run("configure", options) + self.run("configure", options, updated) def done(self): self.run("done") @@ -161,26 +189,27 @@ class ScriptLoader(): """ An addon that manages loading scripts from options. """ - def configure(self, options): - for s in options.scripts: - if options.scripts.count(s) > 1: - raise exceptions.OptionsError("Duplicate script: %s" % s) - - for a in ctx.master.addons.chain[:]: - if isinstance(a, Script) and a.name not in options.scripts: - ctx.log.info("Un-loading script: %s" % a.name) - ctx.master.addons.remove(a) - - current = {} - for a in ctx.master.addons.chain[:]: - if isinstance(a, Script): - current[a.name] = a - ctx.master.addons.chain.remove(a) - - for s in options.scripts: - if s in current: - ctx.master.addons.chain.append(current[s]) - else: - ctx.log.info("Loading script: %s" % s) - sc = Script(s) - ctx.master.addons.add(sc) + def configure(self, options, updated): + if "scripts" in updated: + for s in options.scripts: + if options.scripts.count(s) > 1: + raise exceptions.OptionsError("Duplicate script: %s" % s) + + for a in ctx.master.addons.chain[:]: + if isinstance(a, Script) and a.name not in options.scripts: + ctx.log.info("Un-loading script: %s" % a.name) + ctx.master.addons.remove(a) + + current = {} + for a in ctx.master.addons.chain[:]: + if isinstance(a, Script): + current[a.name] = a + ctx.master.addons.chain.remove(a) + + for s in options.scripts: + if s in current: + ctx.master.addons.chain.append(current[s]) + else: + ctx.log.info("Loading script: %s" % s) + sc = Script(s) + ctx.master.addons.add(options, sc) diff --git a/mitmproxy/builtins/setheaders.py b/mitmproxy/builtins/setheaders.py index 6bda3f55..4a784a1d 100644 --- a/mitmproxy/builtins/setheaders.py +++ b/mitmproxy/builtins/setheaders.py @@ -6,7 +6,7 @@ class SetHeaders: def __init__(self): self.lst = [] - def configure(self, options): + def configure(self, options, updated): """ options.setheaders is a tuple of (fpatt, header, value) diff --git a/mitmproxy/builtins/stickyauth.py b/mitmproxy/builtins/stickyauth.py index 1309911c..98fb65ed 100644 --- a/mitmproxy/builtins/stickyauth.py +++ b/mitmproxy/builtins/stickyauth.py @@ -10,7 +10,7 @@ class StickyAuth: self.flt = None self.hosts = {} - def configure(self, options): + def configure(self, options, updated): if options.stickyauth: flt = filt.parse(options.stickyauth) if not flt: diff --git a/mitmproxy/builtins/stickycookie.py b/mitmproxy/builtins/stickycookie.py index dc699bb4..88333d5c 100644 --- a/mitmproxy/builtins/stickycookie.py +++ b/mitmproxy/builtins/stickycookie.py @@ -32,7 +32,7 @@ class StickyCookie: self.jar = collections.defaultdict(dict) self.flt = None - def configure(self, options): + def configure(self, options, updated): if options.stickycookie: flt = filt.parse(options.stickycookie) if not flt: diff --git a/mitmproxy/console/common.py b/mitmproxy/console/common.py index 281fd658..9fb8b5c9 100644 --- a/mitmproxy/console/common.py +++ b/mitmproxy/console/common.py @@ -134,7 +134,11 @@ def save_data(path, data): if not path: return try: - with open(path, "wb") as f: + if isinstance(data, bytes): + mode = "wb" + else: + mode = "w" + with open(path, mode) as f: f.write(data) except IOError as v: signals.status_message.send(message=v.strerror) @@ -193,10 +197,9 @@ def ask_scope_and_callback(flow, cb, *args): def copy_to_clipboard_or_prompt(data): # pyperclip calls encode('utf-8') on data to be copied without checking. # if data are already encoded that way UnicodeDecodeError is thrown. - toclip = "" - try: - toclip = data.decode('utf-8') - except (UnicodeDecodeError): + if isinstance(data, bytes): + toclip = data.decode("utf8", "replace") + else: toclip = data try: @@ -216,7 +219,7 @@ def copy_to_clipboard_or_prompt(data): def format_flow_data(key, scope, flow): - data = "" + data = b"" if scope in ("q", "b"): request = flow.request.copy() request.decode(strict=False) @@ -230,7 +233,7 @@ def format_flow_data(key, scope, flow): raise ValueError("Unknown key: {}".format(key)) if scope == "b" and flow.request.raw_content and flow.response: # Add padding between request and response - data += "\r\n" * 2 + data += b"\r\n" * 2 if scope in ("s", "b") and flow.response: response = flow.response.copy() response.decode(strict=False) @@ -293,7 +296,7 @@ def ask_save_body(scope, flow): ) elif scope == "b" and request_has_content and response_has_content: ask_save_path( - (flow.request.get_content(strict=False) + "\n" + + (flow.request.get_content(strict=False) + b"\n" + flow.response.get_content(strict=False)), "Save request & response content to" ) @@ -407,7 +410,7 @@ def raw_format_flow(f, focus, extended): return urwid.Pile(pile) -def format_flow(f, focus, extended=False, hostheader=False, marked=False): +def format_flow(f, focus, extended=False, hostheader=False): d = dict( intercepted = f.intercepted, acked = f.reply.acked, @@ -420,7 +423,7 @@ def format_flow(f, focus, extended=False, hostheader=False, marked=False): err_msg = f.error.msg if f.error else None, - marked = marked, + marked = f.marked, ) if f.response: if f.response.raw_content: diff --git a/mitmproxy/console/flowlist.py b/mitmproxy/console/flowlist.py index 53e934f1..43742083 100644 --- a/mitmproxy/console/flowlist.py +++ b/mitmproxy/console/flowlist.py @@ -120,23 +120,17 @@ class ConnectionItem(urwid.WidgetWrap): self.flow, self.f, hostheader = self.master.options.showhost, - marked=self.state.flow_marked(self.flow) ) def selectable(self): return True def save_flows_prompt(self, k): - if k == "a": + if k == "l": signals.status_prompt_path.send( - prompt = "Save all flows to", + prompt = "Save listed flows to", callback = self.master.save_flows ) - elif k == "m": - signals.status_prompt_path.send( - prompt = "Save marked flows to", - callback = self.master.save_marked_flows - ) else: signals.status_prompt_path.send( prompt = "Save this flow to", @@ -188,17 +182,16 @@ class ConnectionItem(urwid.WidgetWrap): self.flow.accept_intercept(self.master) signals.flowlist_change.send(self) elif key == "d": - self.flow.kill(self.master) + if not self.flow.reply.acked: + self.flow.kill(self.master) self.state.delete_flow(self.flow) signals.flowlist_change.send(self) elif key == "D": f = self.master.duplicate_flow(self.flow) - self.master.view_flow(f) + self.master.state.set_focus_flow(f) + signals.flowlist_change.send(self) elif key == "m": - if self.state.flow_marked(self.flow): - self.state.set_flow_marked(self.flow, False) - else: - self.state.set_flow_marked(self.flow, True) + self.flow.marked = not self.flow.marked signals.flowlist_change.send(self) elif key == "M": if self.state.mark_filter: @@ -233,7 +226,7 @@ class ConnectionItem(urwid.WidgetWrap): ) elif key == "U": for f in self.state.flows: - self.state.set_flow_marked(f, False) + f.marked = False signals.flowlist_change.send(self) elif key == "V": if not self.flow.modified(): @@ -247,14 +240,14 @@ class ConnectionItem(urwid.WidgetWrap): self, prompt = "Save", keys = ( - ("all flows", "a"), + ("listed flows", "l"), ("this flow", "t"), - ("marked flows", "m"), ), callback = self.save_flows_prompt, ) elif key == "X": - self.flow.kill(self.master) + if not self.flow.reply.acked: + self.flow.kill(self.master) elif key == "enter": if self.flow.request: self.master.view_flow(self.flow) @@ -356,7 +349,8 @@ class FlowListBox(urwid.ListBox): return scheme, host, port, path = parts f = self.master.create_request(method, scheme, host, port, path) - self.master.view_flow(f) + self.master.state.set_focus_flow(f) + signals.flowlist_change.send(self) def keypress(self, size, key): key = common.shortcuts(key) diff --git a/mitmproxy/console/flowview.py b/mitmproxy/console/flowview.py index 938c8e86..c354563f 100644 --- a/mitmproxy/console/flowview.py +++ b/mitmproxy/console/flowview.py @@ -6,6 +6,7 @@ import sys import traceback import urwid +from typing import Optional, Union # noqa from mitmproxy import contentviews from mitmproxy import controller @@ -38,7 +39,7 @@ def _mkhelp(): ("d", "delete flow"), ("e", "edit request/response"), ("f", "load full body data"), - ("m", "change body display mode for this entity"), + ("m", "change body display mode for this entity\n(default mode can be changed in the options)"), (None, common.highlight_key("automatic", "a") + [("text", ": automatic detection")] @@ -75,7 +76,6 @@ def _mkhelp(): common.highlight_key("xml", "x") + [("text", ": XML")] ), - ("M", "change default body display mode"), ("E", "export flow to file"), ("r", "replay request"), ("V", "revert changes to request"), @@ -105,7 +105,8 @@ footer = [ class FlowViewHeader(urwid.WidgetWrap): def __init__(self, master, f): - self.master, self.flow = master, f + self.master = master # type: "mitmproxy.console.master.ConsoleMaster" + self.flow = f # type: models.HTTPFlow self._w = common.format_flow( f, False, @@ -135,14 +136,15 @@ class FlowView(tabs.Tabs): def __init__(self, master, state, flow, tab_offset): self.master, self.state, self.flow = master, state, flow - tabs.Tabs.__init__(self, - [ - (self.tab_request, self.view_request), - (self.tab_response, self.view_response), - (self.tab_details, self.view_details), - ], - tab_offset - ) + super(FlowView, self).__init__( + [ + (self.tab_request, self.view_request), + (self.tab_response, self.view_response), + (self.tab_details, self.view_details), + ], + tab_offset + ) + self.show() self.last_displayed_body = None signals.flow_change.connect(self.sig_flow_change) @@ -189,15 +191,21 @@ class FlowView(tabs.Tabs): limit = sys.maxsize else: limit = contentviews.VIEW_CUTOFF + + flow_modify_cache_invalidation = hash(( + message.raw_content, + message.headers.fields, + getattr(message, "path", None), + )) return cache.get( - self._get_content_view, + # We move message into this partial function as it is not hashable. + lambda *args: self._get_content_view(message, *args), viewmode, - message, limit, - message # Cache invalidation + flow_modify_cache_invalidation ) - def _get_content_view(self, viewmode, message, max_lines, _): + def _get_content_view(self, message, viewmode, max_lines, _): try: content = message.content @@ -396,7 +404,7 @@ class FlowView(tabs.Tabs): if not self.flow.response: self.flow.response = models.HTTPResponse( self.flow.request.http_version, - 200, "OK", Headers(), "" + 200, b"OK", Headers(), b"" ) self.flow.response.reply = controller.DummyReply() message = self.flow.response @@ -524,30 +532,24 @@ class FlowView(tabs.Tabs): ) signals.flow_change.send(self, flow = self.flow) - def delete_body(self, t): + def keypress(self, size, key): + conn = None # type: Optional[Union[models.HTTPRequest, models.HTTPResponse]] if self.tab_offset == TAB_REQ: - self.flow.request.content = None - else: - self.flow.response.content = None - signals.flow_change.send(self, flow = self.flow) + conn = self.flow.request + elif self.tab_offset == TAB_RESP: + conn = self.flow.response - def keypress(self, size, key): key = super(self.__class__, self).keypress(size, key) + # Special case: Space moves over to the next flow. + # We need to catch that before applying common.shortcuts() if key == " ": self.view_next_flow(self.flow) return key = common.shortcuts(key) - if self.tab_offset == TAB_REQ: - conn = self.flow.request - elif self.tab_offset == TAB_RESP: - conn = self.flow.response - else: - conn = None - if key in ("up", "down", "page up", "page down"): - # Why doesn't this just work?? + # Pass scroll events to the wrapped widget self._w.keypress(size, key) elif key == "a": self.flow.accept_intercept(self.master) @@ -563,10 +565,12 @@ class FlowView(tabs.Tabs): else: self.view_next_flow(self.flow) f = self.flow - f.kill(self.master) + if not f.reply.acked: + f.kill(self.master) self.state.delete_flow(f) elif key == "D": f = self.master.duplicate_flow(self.flow) + signals.pop_view_state.send(self) self.master.view_flow(f) signals.status_message.send(message="Duplicated.") elif key == "p": @@ -577,12 +581,12 @@ class FlowView(tabs.Tabs): signals.status_message.send(message=r) signals.flow_change.send(self, flow = self.flow) elif key == "V": - if not self.flow.modified(): + if self.flow.modified(): + self.state.revert(self.flow) + signals.flow_change.send(self, flow = self.flow) + signals.status_message.send(message="Reverted.") + else: signals.status_message.send(message="Flow not modified.") - return - self.state.revert(self.flow) - signals.flow_change.send(self, flow = self.flow) - signals.status_message.send(message="Reverted.") elif key == "W": signals.status_prompt_path.send( prompt = "Save this flow", @@ -595,133 +599,128 @@ class FlowView(tabs.Tabs): callback = self.master.run_script_once, args = (self.flow,) ) - - if not conn and key in set(list("befgmxvzEC")): + elif key == "e": + if self.tab_offset == TAB_REQ: + signals.status_prompt_onekey.send( + prompt="Edit request", + keys=( + ("cookies", "c"), + ("query", "q"), + ("path", "p"), + ("url", "u"), + ("header", "h"), + ("form", "f"), + ("raw body", "r"), + ("method", "m"), + ), + callback=self.edit + ) + elif self.tab_offset == TAB_RESP: + signals.status_prompt_onekey.send( + prompt="Edit response", + keys=( + ("cookies", "c"), + ("code", "o"), + ("message", "m"), + ("header", "h"), + ("raw body", "r"), + ), + callback=self.edit + ) + else: + signals.status_message.send( + message="Tab to the request or response", + expire=1 + ) + elif key in set("bfgmxvzEC") and not conn: signals.status_message.send( message = "Tab to the request or response", expire = 1 ) - elif conn: - if key == "b": - if self.tab_offset == TAB_REQ: - common.ask_save_body( - "q", self.master, self.state, self.flow - ) + return + elif key == "b": + if self.tab_offset == TAB_REQ: + common.ask_save_body("q", self.flow) + else: + common.ask_save_body("s", self.flow) + elif key == "f": + signals.status_message.send(message="Loading all body data...") + self.state.add_flow_setting( + self.flow, + (self.tab_offset, "fullcontents"), + True + ) + signals.flow_change.send(self, flow = self.flow) + signals.status_message.send(message="") + elif key == "m": + p = list(contentviews.view_prompts) + p.insert(0, ("Clear", "C")) + signals.status_prompt_onekey.send( + self, + prompt = "Display mode", + keys = p, + callback = self.change_this_display_mode + ) + elif key == "E": + if self.tab_offset == TAB_REQ: + scope = "q" + else: + scope = "s" + signals.status_prompt_onekey.send( + self, + prompt = "Export to file", + keys = [(e[0], e[1]) for e in export.EXPORTERS], + callback = common.export_to_clip_or_file, + args = (scope, self.flow, common.ask_save_path) + ) + elif key == "C": + if self.tab_offset == TAB_REQ: + scope = "q" + else: + scope = "s" + signals.status_prompt_onekey.send( + self, + prompt = "Export to clipboard", + keys = [(e[0], e[1]) for e in export.EXPORTERS], + callback = common.export_to_clip_or_file, + args = (scope, self.flow, common.copy_to_clipboard_or_prompt) + ) + elif key == "x": + conn.content = None + signals.flow_change.send(self, flow=self.flow) + elif key == "v": + if conn.raw_content: + t = conn.headers.get("content-type") + if "EDITOR" in os.environ or "PAGER" in os.environ: + self.master.spawn_external_viewer(conn.get_content(strict=False), t) else: - common.ask_save_body( - "s", self.master, self.state, self.flow - ) - elif key == "e": - if self.tab_offset == TAB_REQ: - signals.status_prompt_onekey.send( - prompt = "Edit request", - keys = ( - ("cookies", "c"), - ("query", "q"), - ("path", "p"), - ("url", "u"), - ("header", "h"), - ("form", "f"), - ("raw body", "r"), - ("method", "m"), - ), - callback = self.edit + signals.status_message.send( + message = "Error! Set $EDITOR or $PAGER." ) - else: - signals.status_prompt_onekey.send( - prompt = "Edit response", - keys = ( - ("cookies", "c"), - ("code", "o"), - ("message", "m"), - ("header", "h"), - ("raw body", "r"), - ), - callback = self.edit + elif key == "z": + self.flow.backup() + e = conn.headers.get("content-encoding", "identity") + if e != "identity": + try: + conn.decode() + except ValueError: + signals.status_message.send( + message = "Could not decode - invalid data?" ) - key = None - elif key == "f": - signals.status_message.send(message="Loading all body data...") - self.state.add_flow_setting( - self.flow, - (self.tab_offset, "fullcontents"), - True - ) - signals.flow_change.send(self, flow = self.flow) - signals.status_message.send(message="") - elif key == "m": - p = list(contentviews.view_prompts) - p.insert(0, ("Clear", "C")) - signals.status_prompt_onekey.send( - self, - prompt = "Display mode", - keys = p, - callback = self.change_this_display_mode - ) - key = None - elif key == "E": - if self.tab_offset == TAB_REQ: - scope = "q" - else: - scope = "s" - signals.status_prompt_onekey.send( - self, - prompt = "Export to file", - keys = [(e[0], e[1]) for e in export.EXPORTERS], - callback = common.export_to_clip_or_file, - args = (scope, self.flow, common.ask_save_path) - ) - elif key == "C": - if self.tab_offset == TAB_REQ: - scope = "q" - else: - scope = "s" - signals.status_prompt_onekey.send( - self, - prompt = "Export to clipboard", - keys = [(e[0], e[1]) for e in export.EXPORTERS], - callback = common.export_to_clip_or_file, - args = (scope, self.flow, common.copy_to_clipboard_or_prompt) - ) - elif key == "x": + else: signals.status_prompt_onekey.send( - prompt = "Delete body", + prompt = "Select encoding: ", keys = ( - ("completely", "c"), - ("mark as missing", "m"), + ("gzip", "z"), + ("deflate", "d"), ), - callback = self.delete_body + callback = self.encode_callback, + args = (conn,) ) - key = None - elif key == "v": - if conn.raw_content: - t = conn.headers.get("content-type") - if "EDITOR" in os.environ or "PAGER" in os.environ: - self.master.spawn_external_viewer(conn.get_content(strict=False), t) - else: - signals.status_message.send( - message = "Error! Set $EDITOR or $PAGER." - ) - elif key == "z": - self.flow.backup() - e = conn.headers.get("content-encoding", "identity") - if e != "identity": - if not conn.decode(): - signals.status_message.send( - message = "Could not decode - invalid data?" - ) - else: - signals.status_prompt_onekey.send( - prompt = "Select encoding: ", - keys = ( - ("gzip", "z"), - ("deflate", "d"), - ), - callback = self.encode_callback, - args = (conn,) - ) - signals.flow_change.send(self, flow = self.flow) - return key + signals.flow_change.send(self, flow = self.flow) + else: + # Key is not handled here. + return key def encode_callback(self, key, conn): encoding_map = { diff --git a/mitmproxy/console/help.py b/mitmproxy/console/help.py index 064d3cb5..ff4a072f 100644 --- a/mitmproxy/console/help.py +++ b/mitmproxy/console/help.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, print_function, division +import platform + import urwid from mitmproxy import filt @@ -9,7 +11,7 @@ from mitmproxy.console import signals from netlib import version footer = [ - ("heading", 'mitmproxy v%s ' % version.VERSION), + ("heading", 'mitmproxy {} (Python {}) '.format(version.VERSION, platform.python_version())), ('heading_key', "q"), ":back ", ] diff --git a/mitmproxy/console/master.py b/mitmproxy/console/master.py index 4fd6cb78..db414147 100644 --- a/mitmproxy/console/master.py +++ b/mitmproxy/console/master.py @@ -34,6 +34,7 @@ from mitmproxy.console import palettes from mitmproxy.console import signals from mitmproxy.console import statusbar from mitmproxy.console import window +from mitmproxy.filt import FMarked from netlib import tcp, strutils EVENTLOG_SIZE = 500 @@ -48,7 +49,7 @@ class ConsoleState(flow.State): self.default_body_view = contentviews.get("Auto") self.flowsettings = weakref.WeakKeyDictionary() self.last_search = None - self.last_filter = None + self.last_filter = "" self.mark_filter = False def __setattr__(self, name, value): @@ -66,7 +67,6 @@ class ConsoleState(flow.State): def add_flow(self, f): super(ConsoleState, self).add_flow(f) self.update_focus() - self.set_flow_marked(f, False) return f def update_flow(self, f): @@ -86,10 +86,10 @@ class ConsoleState(flow.State): def set_focus(self, idx): if self.view: - if idx >= len(self.view): - idx = len(self.view) - 1 - elif idx < 0: + if idx is None or idx < 0: idx = 0 + elif idx >= len(self.view): + idx = len(self.view) - 1 self.focus = idx else: self.focus = None @@ -123,48 +123,71 @@ class ConsoleState(flow.State): self.set_focus(self.focus) return ret - def filter_marked(self, m): - def actual_func(x): - if x.id in m: - return True - return False - return actual_func + def get_nearest_matching_flow(self, flow, filt): + fidx = self.view.index(flow) + dist = 1 + + fprev = fnext = True + while fprev or fnext: + fprev, _ = self.get_from_pos(fidx - dist) + fnext, _ = self.get_from_pos(fidx + dist) + + if fprev and fprev.match(filt): + return fprev + elif fnext and fnext.match(filt): + return fnext + + dist += 1 + + return None def enable_marked_filter(self): + marked_flows = [f for f in self.flows if f.marked] + if not marked_flows: + return + + marked_filter = "~%s" % FMarked.code + + # Save Focus + last_focus, _ = self.get_focus() + nearest_marked = self.get_nearest_matching_flow(last_focus, marked_filter) + self.last_filter = self.limit_txt - marked_flows = [] - for f in self.flows: - if self.flow_marked(f): - marked_flows.append(f.id) - if len(marked_flows) > 0: - f = self.filter_marked(marked_flows) - self.view._close() - self.view = flow.FlowView(self.flows, f) - self.focus = 0 - self.set_focus(self.focus) - self.mark_filter = True + self.set_limit(marked_filter) + + # Restore Focus + if last_focus.marked: + self.set_focus_flow(last_focus) + else: + self.set_focus_flow(nearest_marked) + + self.mark_filter = True def disable_marked_filter(self): - if self.last_filter is None: - self.view = flow.FlowView(self.flows, None) + marked_filter = "~%s" % FMarked.code + + # Save Focus + last_focus, _ = self.get_focus() + nearest_marked = self.get_nearest_matching_flow(last_focus, marked_filter) + + self.set_limit(self.last_filter) + self.last_filter = "" + + # Restore Focus + if last_focus.marked: + self.set_focus_flow(last_focus) else: - self.set_limit(self.last_filter) - self.focus = 0 - self.set_focus(self.focus) - self.last_filter = None + self.set_focus_flow(nearest_marked) + self.mark_filter = False def clear(self): - marked_flows = [] - for f in self.flows: - if self.flow_marked(f): - marked_flows.append(f) - + marked_flows = [f for f in self.view if f.marked] super(ConsoleState, self).clear() for f in marked_flows: self.add_flow(f) - self.set_flow_marked(f, True) + f.marked = True if len(self.flows.views) == 0: self.focus = None @@ -172,12 +195,6 @@ class ConsoleState(flow.State): self.focus = 0 self.set_focus(self.focus) - def flow_marked(self, flow): - return self.get_flow_setting(flow, "marked", False) - - def set_flow_marked(self, flow, marked): - self.add_flow_setting(flow, "marked", marked) - class Options(mitmproxy.options.Options): def __init__( @@ -242,7 +259,7 @@ class ConsoleMaster(flow.FlowMaster): signals.pop_view_state.connect(self.sig_pop_view_state) signals.push_view_state.connect(self.sig_push_view_state) signals.sig_add_log.connect(self.sig_add_log) - self.addons.add(*builtins.default_addons()) + self.addons.add(options, *builtins.default_addons()) def __setattr__(self, name, value): self.__dict__[name] = value @@ -254,10 +271,6 @@ class ConsoleMaster(flow.FlowMaster): expire=1 ) - def load_script(self, command, use_reloader=True): - # We default to using the reloader in the console ui. - return super(ConsoleMaster, self).load_script(command, use_reloader) - def sig_add_log(self, sender, e, level): if self.options.verbosity < utils.log_tier(level): return @@ -352,7 +365,7 @@ class ConsoleMaster(flow.FlowMaster): try: return flow.read_flows_from_paths(path) except exceptions.FlowReadException as e: - signals.status_message.send(message=e.strerror) + signals.status_message.send(message=str(e)) def client_playback_path(self, path): if not isinstance(path, list): @@ -619,13 +632,6 @@ class ConsoleMaster(flow.FlowMaster): def save_flows(self, path): return self._write_flows(path, self.state.view) - def save_marked_flows(self, path): - marked_flows = [] - for f in self.state.view: - if self.state.flow_marked(f): - marked_flows.append(f) - return self._write_flows(path, marked_flows) - def load_flows_callback(self, path): if not path: return @@ -748,10 +754,3 @@ class ConsoleMaster(flow.FlowMaster): direction=direction, ), "info") self.add_log(strutils.bytes_to_escaped_str(message.content), "debug") - - @controller.handler - def script_change(self, script): - if super(ConsoleMaster, self).script_change(script): - signals.status_message.send(message='"{}" reloaded.'.format(script.path)) - else: - signals.status_message.send(message='Error reloading "{}".'.format(script.path)) diff --git a/mitmproxy/console/options.py b/mitmproxy/console/options.py index 62564a60..f9fc3764 100644 --- a/mitmproxy/console/options.py +++ b/mitmproxy/console/options.py @@ -140,7 +140,7 @@ class Options(urwid.WidgetWrap): ) self.master.loop.widget.footer.update("") signals.update_settings.connect(self.sig_update_settings) - master.options.changed.connect(self.sig_update_settings) + master.options.changed.connect(lambda sender, updated: self.sig_update_settings(sender)) def sig_update_settings(self, sender): self.lb.walker._modified() diff --git a/mitmproxy/console/searchable.py b/mitmproxy/console/searchable.py index c60d1cd9..d58d3d13 100644 --- a/mitmproxy/console/searchable.py +++ b/mitmproxy/console/searchable.py @@ -78,9 +78,9 @@ class Searchable(urwid.ListBox): return # Start search at focus + 1 if backwards: - rng = xrange(len(self.body) - 1, -1, -1) + rng = range(len(self.body) - 1, -1, -1) else: - rng = xrange(1, len(self.body) + 1) + rng = range(1, len(self.body) + 1) for i in rng: off = (self.focus_position + i) % len(self.body) w = self.body[off] diff --git a/mitmproxy/console/statusbar.py b/mitmproxy/console/statusbar.py index 3120fa71..156d1176 100644 --- a/mitmproxy/console/statusbar.py +++ b/mitmproxy/console/statusbar.py @@ -124,7 +124,7 @@ class StatusBar(urwid.WidgetWrap): super(StatusBar, self).__init__(urwid.Pile([self.ib, self.master.ab])) signals.update_settings.connect(self.sig_update_settings) signals.flowlist_change.connect(self.sig_update_settings) - master.options.changed.connect(self.sig_update_settings) + master.options.changed.connect(lambda sender, updated: self.sig_update_settings(sender)) self.redraw() def sig_update_settings(self, sender): @@ -171,10 +171,6 @@ class StatusBar(urwid.WidgetWrap): r.append("[") r.append(("heading_key", "l")) r.append(":%s]" % self.master.state.limit_txt) - if self.master.state.mark_filter: - r.append("[") - r.append(("heading_key", "Marked Flows")) - r.append("]") if self.master.options.stickycookie: r.append("[") r.append(("heading_key", "t")) diff --git a/mitmproxy/console/tabs.py b/mitmproxy/console/tabs.py index bfcdeba3..a5e9c510 100644 --- a/mitmproxy/console/tabs.py +++ b/mitmproxy/console/tabs.py @@ -25,7 +25,7 @@ class Tab(urwid.WidgetWrap): class Tabs(urwid.WidgetWrap): def __init__(self, tabs, tab_offset=0): - urwid.WidgetWrap.__init__(self, "") + super(Tabs, self).__init__("") self.tab_offset = tab_offset self.tabs = tabs self.show() diff --git a/mitmproxy/contentviews.py b/mitmproxy/contentviews.py index afdaad7f..e155bc01 100644 --- a/mitmproxy/contentviews.py +++ b/mitmproxy/contentviews.py @@ -20,6 +20,8 @@ import logging import subprocess import sys +from typing import Mapping # noqa + import html2text import lxml.etree import lxml.html @@ -76,6 +78,7 @@ def pretty_json(s): def format_dict(d): + # type: (Mapping[Union[str,bytes], Union[str,bytes]]) -> Generator[Tuple[Union[str,bytes], Union[str,bytes]]] """ Helper function that transforms the given dictionary into a list of ("key", key ) @@ -85,7 +88,7 @@ def format_dict(d): max_key_len = max(len(k) for k in d.keys()) max_key_len = min(max_key_len, KEY_MAX) for key, value in d.items(): - key += ":" + key += b":" if isinstance(key, bytes) else u":" key = key.ljust(max_key_len + 2) yield [ ("header", key), @@ -106,12 +109,16 @@ class View(object): prompt = () content_types = [] - def __call__(self, data, **metadata): + def __call__( + self, + data, # type: bytes + **metadata + ): """ Transform raw data into human-readable output. Args: - data: the data to decode/format as bytes. + data: the data to decode/format. metadata: optional keyword-only arguments for metadata. Implementations must not rely on a given argument being present. @@ -278,6 +285,10 @@ class ViewURLEncoded(View): content_types = ["application/x-www-form-urlencoded"] def __call__(self, data, **metadata): + try: + data = data.decode("ascii", "strict") + except ValueError: + return None d = url.decode(data) return "URLEncoded form", format_dict(multidict.MultiDict(d)) diff --git a/mitmproxy/controller.py b/mitmproxy/controller.py index 070ec862..35817a85 100644 --- a/mitmproxy/controller.py +++ b/mitmproxy/controller.py @@ -37,8 +37,6 @@ Events = frozenset([ "configure", "done", "tick", - - "script_change", ]) diff --git a/mitmproxy/ctx.py b/mitmproxy/ctx.py index fcfdfd0b..5d2905fa 100644 --- a/mitmproxy/ctx.py +++ b/mitmproxy/ctx.py @@ -1,4 +1,4 @@ from typing import Callable # noqa master = None # type: "mitmproxy.flow.FlowMaster" -log = None # type: Callable[[str], None] +log = None # type: "mitmproxy.controller.Log" diff --git a/mitmproxy/dump.py b/mitmproxy/dump.py index 4f34ab95..83f44d87 100644 --- a/mitmproxy/dump.py +++ b/mitmproxy/dump.py @@ -42,8 +42,8 @@ class DumpMaster(flow.FlowMaster): def __init__(self, server, options): flow.FlowMaster.__init__(self, options, server, flow.State()) self.has_errored = False - self.addons.add(*builtins.default_addons()) - self.addons.add(dumper.Dumper()) + self.addons.add(options, *builtins.default_addons()) + self.addons.add(options, dumper.Dumper()) # This line is just for type hinting self.options = self.options # type: Options self.replay_ignore_params = options.replay_ignore_params diff --git a/mitmproxy/filt.py b/mitmproxy/filt.py index 8b647b22..67915e5b 100644 --- a/mitmproxy/filt.py +++ b/mitmproxy/filt.py @@ -39,9 +39,12 @@ import functools from mitmproxy.models.http import HTTPFlow from mitmproxy.models.tcp import TCPFlow +from mitmproxy.models.flow import Flow + from netlib import strutils import pyparsing as pp +from typing import Callable def only(*types): @@ -80,6 +83,14 @@ class FErr(_Action): return True if f.error else False +class FMarked(_Action): + code = "marked" + help = "Match marked flows" + + def __call__(self, f): + return f.marked + + class FHTTP(_Action): code = "http" help = "Match HTTP flows" @@ -398,6 +409,7 @@ filt_unary = [ FAsset, FErr, FHTTP, + FMarked, FReq, FResp, FTCP, @@ -471,7 +483,11 @@ def _make(): bnf = _make() +TFilter = Callable[[Flow], bool] + + def parse(s): + # type: (str) -> TFilter try: filt = bnf.parseString(s, parseAll=True)[0] filt.pattern = s diff --git a/mitmproxy/flow/io_compat.py b/mitmproxy/flow/io_compat.py index 8cd883c3..061bf16d 100644 --- a/mitmproxy/flow/io_compat.py +++ b/mitmproxy/flow/io_compat.py @@ -60,6 +60,7 @@ def convert_017_018(data): data = convert_unicode(data) data["server_conn"]["ip_address"] = data["server_conn"].pop("peer_address") + data["marked"] = False data["version"] = (0, 18) return data diff --git a/mitmproxy/models/flow.py b/mitmproxy/models/flow.py index f4993b7a..f4a2b54b 100644 --- a/mitmproxy/models/flow.py +++ b/mitmproxy/models/flow.py @@ -8,6 +8,8 @@ from mitmproxy import stateobject from mitmproxy.models.connections import ClientConnection from mitmproxy.models.connections import ServerConnection +import six + from netlib import version from typing import Optional # noqa @@ -79,6 +81,7 @@ class Flow(stateobject.StateObject): self.intercepted = False # type: bool self._backup = None # type: Optional[Flow] self.reply = None + self.marked = False # type: bool _stateobject_attributes = dict( id=str, @@ -86,7 +89,8 @@ class Flow(stateobject.StateObject): client_conn=ClientConnection, server_conn=ServerConnection, type=str, - intercepted=bool + intercepted=bool, + marked=bool, ) def get_state(self): @@ -173,3 +177,21 @@ class Flow(stateobject.StateObject): self.intercepted = False self.reply.ack() master.handle_accept_intercept(self) + + def match(self, f): + """ + Match this flow against a compiled filter expression. Returns True + if matched, False if not. + + If f is a string, it will be compiled as a filter expression. If + the expression is invalid, ValueError is raised. + """ + if isinstance(f, six.string_types): + from .. import filt + + f = filt.parse(f) + if not f: + raise ValueError("Invalid filter expression.") + if f: + return f(self) + return True diff --git a/mitmproxy/models/http.py b/mitmproxy/models/http.py index 1fd28f00..7781e61f 100644 --- a/mitmproxy/models/http.py +++ b/mitmproxy/models/http.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, print_function, division import cgi import warnings -import six from mitmproxy.models.flow import Flow from netlib import version @@ -211,24 +210,6 @@ class HTTPFlow(Flow): f.response = self.response.copy() return f - def match(self, f): - """ - Match this flow against a compiled filter expression. Returns True - if matched, False if not. - - If f is a string, it will be compiled as a filter expression. If - the expression is invalid, ValueError is raised. - """ - if isinstance(f, six.string_types): - from .. import filt - - f = filt.parse(f) - if not f: - raise ValueError("Invalid filter expression.") - if f: - return f(self) - return True - def replace(self, pattern, repl, *args, **kwargs): """ Replaces a regular expression pattern with repl in both request and diff --git a/mitmproxy/models/tcp.py b/mitmproxy/models/tcp.py index 6650141d..e33475c2 100644 --- a/mitmproxy/models/tcp.py +++ b/mitmproxy/models/tcp.py @@ -7,8 +7,6 @@ from typing import List import netlib.basetypes from mitmproxy.models.flow import Flow -import six - class TCPMessage(netlib.basetypes.Serializable): @@ -55,22 +53,3 @@ class TCPFlow(Flow): def __repr__(self): return "<TCPFlow ({} messages)>".format(len(self.messages)) - - def match(self, f): - """ - Match this flow against a compiled filter expression. Returns True - if matched, False if not. - - If f is a string, it will be compiled as a filter expression. If - the expression is invalid, ValueError is raised. - """ - if isinstance(f, six.string_types): - from .. import filt - - f = filt.parse(f) - if not f: - raise ValueError("Invalid filter expression.") - if f: - return f(self) - - return True diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index e94ef51d..140c7ca8 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -35,7 +35,7 @@ class OptManager(object): self.__dict__["_initialized"] = True @contextlib.contextmanager - def rollback(self): + def rollback(self, updated): old = self._opts.copy() try: yield @@ -44,7 +44,7 @@ class OptManager(object): self.errored.send(self, exc=e) # Rollback self.__dict__["_opts"] = old - self.changed.send(self) + self.changed.send(self, updated=updated) def __eq__(self, other): return self._opts == other._opts @@ -62,22 +62,22 @@ class OptManager(object): if not self._initialized: self._opts[attr] = value return - if attr not in self._opts: - raise KeyError("No such option: %s" % attr) - with self.rollback(): - self._opts[attr] = value - self.changed.send(self) + self.update(**{attr: value}) + + def keys(self): + return set(self._opts.keys()) def get(self, k, d=None): return self._opts.get(k, d) def update(self, **kwargs): + updated = set(kwargs.keys()) for k in kwargs: if k not in self._opts: raise KeyError("No such option: %s" % k) - with self.rollback(): + with self.rollback(updated): self._opts.update(kwargs) - self.changed.send(self) + self.changed.send(self, updated=updated) def setter(self, attr): """ diff --git a/mitmproxy/protocol/http2.py b/mitmproxy/protocol/http2.py index 1285e10e..8308f44d 100644 --- a/mitmproxy/protocol/http2.py +++ b/mitmproxy/protocol/http2.py @@ -584,6 +584,8 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) except exceptions.ProtocolException as e: # pragma: no cover self.log(repr(e), "info") self.log(traceback.format_exc(), "debug") + except exceptions.Kill: + self.log("Connection killed", "info") if not self.zombie: self.zombie = time.time() diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 7aa4c736..a74ba7e2 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -79,10 +79,10 @@ class ProxyConfig: self.certstore = None self.clientcerts = None self.openssl_verification_mode_server = None - self.configure(options) + self.configure(options, set(options.keys())) options.changed.connect(self.configure) - def configure(self, options): + def configure(self, options, updated): conflict = all( [ options.add_upstream_certs_to_client_chain, diff --git a/mitmproxy/web/app.py b/mitmproxy/web/app.py index 8ccc21c5..f8f85f3d 100644 --- a/mitmproxy/web/app.py +++ b/mitmproxy/web/app.py @@ -234,7 +234,8 @@ class AcceptFlow(RequestHandler): class FlowHandler(RequestHandler): def delete(self, flow_id): - self.flow.kill(self.master) + if not self.flow.reply.acked: + self.flow.kill(self.master) self.state.delete_flow(self.flow) def put(self, flow_id): diff --git a/mitmproxy/web/master.py b/mitmproxy/web/master.py index 3d384612..9ddb61d4 100644 --- a/mitmproxy/web/master.py +++ b/mitmproxy/web/master.py @@ -136,7 +136,7 @@ class WebMaster(flow.FlowMaster): def __init__(self, server, options): super(WebMaster, self).__init__(options, server, WebState()) - self.addons.add(*builtins.default_addons()) + self.addons.add(options, *builtins.default_addons()) self.app = app.Application( self, self.options.wdebug, self.options.wauthenticator ) |