aboutsummaryrefslogtreecommitdiffstats
path: root/mitmproxy
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2016-07-25 15:16:16 -0700
committerMaximilian Hils <git@maximilianhils.com>2016-07-25 15:16:16 -0700
commit79ebcb046e8669f80357a6c3046ec76c6adf49be (patch)
tree441981a16f1be1e620584e4a47f41767ce5585b2 /mitmproxy
parent3254595584e1d711e7ae292ad34753a52f7a0fc1 (diff)
parent56796aeda25dda66621ce78af227ff46049ef811 (diff)
downloadmitmproxy-79ebcb046e8669f80357a6c3046ec76c6adf49be.tar.gz
mitmproxy-79ebcb046e8669f80357a6c3046ec76c6adf49be.tar.bz2
mitmproxy-79ebcb046e8669f80357a6c3046ec76c6adf49be.zip
Merge remote-tracking branch 'origin/master' into flow_editing_v2
Diffstat (limited to 'mitmproxy')
-rw-r--r--mitmproxy/addons.py15
-rw-r--r--mitmproxy/builtins/anticache.py2
-rw-r--r--mitmproxy/builtins/anticomp.py2
-rw-r--r--mitmproxy/builtins/dumper.py79
-rw-r--r--mitmproxy/builtins/filestreamer.py2
-rw-r--r--mitmproxy/builtins/replace.py2
-rw-r--r--mitmproxy/builtins/script.py95
-rw-r--r--mitmproxy/builtins/setheaders.py2
-rw-r--r--mitmproxy/builtins/stickyauth.py2
-rw-r--r--mitmproxy/builtins/stickycookie.py2
-rw-r--r--mitmproxy/console/common.py23
-rw-r--r--mitmproxy/console/flowlist.py32
-rw-r--r--mitmproxy/console/flowview.py303
-rw-r--r--mitmproxy/console/help.py4
-rw-r--r--mitmproxy/console/master.py119
-rw-r--r--mitmproxy/console/options.py2
-rw-r--r--mitmproxy/console/searchable.py4
-rw-r--r--mitmproxy/console/statusbar.py6
-rw-r--r--mitmproxy/console/tabs.py2
-rw-r--r--mitmproxy/contentviews.py17
-rw-r--r--mitmproxy/controller.py2
-rw-r--r--mitmproxy/ctx.py2
-rw-r--r--mitmproxy/dump.py4
-rw-r--r--mitmproxy/filt.py16
-rw-r--r--mitmproxy/flow/io_compat.py1
-rw-r--r--mitmproxy/models/flow.py24
-rw-r--r--mitmproxy/models/http.py19
-rw-r--r--mitmproxy/models/tcp.py21
-rw-r--r--mitmproxy/optmanager.py18
-rw-r--r--mitmproxy/protocol/http2.py2
-rw-r--r--mitmproxy/proxy/config.py4
-rw-r--r--mitmproxy/web/app.py3
-rw-r--r--mitmproxy/web/master.py2
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
)