Merge pull request #1353 from cortesi/stream

Streaming to file -> addon
This commit is contained in:
Aldo Cortesi 2016-07-14 14:09:59 +12:00 committed by GitHub
commit b8a23eeaa3
8 changed files with 167 additions and 118 deletions

View File

@ -4,6 +4,7 @@ from mitmproxy.builtins import anticache
from mitmproxy.builtins import anticomp from mitmproxy.builtins import anticomp
from mitmproxy.builtins import stickyauth from mitmproxy.builtins import stickyauth
from mitmproxy.builtins import stickycookie from mitmproxy.builtins import stickycookie
from mitmproxy.builtins import stream
def default_addons(): def default_addons():
@ -12,4 +13,5 @@ def default_addons():
anticomp.AntiComp(), anticomp.AntiComp(),
stickyauth.StickyAuth(), stickyauth.StickyAuth(),
stickycookie.StickyCookie(), stickycookie.StickyCookie(),
stream.Stream(),
] ]

View File

@ -0,0 +1,54 @@
from __future__ import absolute_import, print_function, division
import os.path
from mitmproxy import ctx
from mitmproxy import exceptions
from mitmproxy.flow import io
class Stream:
def __init__(self):
self.stream = None
def start_stream_to_path(self, path, mode, filt):
path = os.path.expanduser(path)
try:
f = open(path, mode)
except IOError as v:
return str(v)
self.stream = io.FilteredFlowWriter(f, filt)
def configure(self, options):
# We're already streaming - stop the previous stream and restart
if self.stream:
self.done()
if options.outfile:
filt = None
if options.get("filtstr"):
filt = filt.parse(options.filtstr)
if not filt:
raise exceptions.OptionsError(
"Invalid filter specification: %s" % options.filtstr
)
path, mode = options.outfile
if mode not in ("wb", "ab"):
raise exceptions.OptionsError("Invalid mode.")
err = self.start_stream_to_path(path, mode, filt)
if err:
raise exceptions.OptionsError(err)
def done(self):
if self.stream:
for flow in ctx.master.active_flows:
self.stream.add(flow)
self.stream.fo.close()
self.stream = None
def tcp_close(self, flow):
if self.stream:
self.stream.add(flow)
def response(self, flow):
if self.stream:
self.stream.add(flow)

View File

@ -52,16 +52,17 @@ class Options(options.Options):
"replay_ignore_content", "replay_ignore_content",
"replay_ignore_params", "replay_ignore_params",
"replay_ignore_payload_params", "replay_ignore_payload_params",
"replay_ignore_host" "replay_ignore_host",
"tfile"
] ]
class DumpMaster(flow.FlowMaster): class DumpMaster(flow.FlowMaster):
def __init__(self, server, options, outfile=None): def __init__(self, server, options):
flow.FlowMaster.__init__(self, options, server, flow.State()) flow.FlowMaster.__init__(self, options, server, flow.State())
self.addons.add(*builtins.default_addons()) self.addons.add(*builtins.default_addons())
self.outfile = outfile
self.o = options self.o = options
self.showhost = options.showhost self.showhost = options.showhost
self.replay_ignore_params = options.replay_ignore_params self.replay_ignore_params = options.replay_ignore_params
@ -82,15 +83,6 @@ class DumpMaster(flow.FlowMaster):
else: else:
self.filt = None self.filt = None
if options.outfile:
err = self.start_stream_to_path(
options.outfile[0],
options.outfile[1],
self.filt
)
if err:
raise DumpError(err)
if options.replacements: if options.replacements:
for i in options.replacements: for i in options.replacements:
self.replacehooks.add(*i) self.replacehooks.add(*i)
@ -163,7 +155,7 @@ class DumpMaster(flow.FlowMaster):
def echo(self, text, indent=None, **style): def echo(self, text, indent=None, **style):
if indent: if indent:
text = self.indent(indent, text) text = self.indent(indent, text)
click.secho(text, file=self.outfile, **style) click.secho(text, file=self.options.tfile, **style)
def _echo_message(self, message): def _echo_message(self, message):
if self.options.flow_detail >= 2 and hasattr(message, "headers"): if self.options.flow_detail >= 2 and hasattr(message, "headers"):
@ -312,8 +304,8 @@ class DumpMaster(flow.FlowMaster):
if f.error: if f.error:
self.echo(" << {}".format(f.error.msg), bold=True, fg="red") self.echo(" << {}".format(f.error.msg), bold=True, fg="red")
if self.outfile: if self.options.tfile:
self.outfile.flush() self.options.tfile.flush()
def _process_flow(self, f): def _process_flow(self, f):
if self.filt and not f.match(self.filt): if self.filt and not f.match(self.filt):

View File

@ -46,7 +46,6 @@ class FlowMaster(controller.Master):
self.replay_ignore_content = None self.replay_ignore_content = None
self.replay_ignore_host = False self.replay_ignore_host = False
self.stream = None
self.apps = modules.AppRegistry() self.apps = modules.AppRegistry()
def start_app(self, host, port): def start_app(self, host, port):
@ -409,8 +408,6 @@ class FlowMaster(controller.Master):
if not f.reply.acked: if not f.reply.acked:
if self.client_playback: if self.client_playback:
self.client_playback.clear(f) self.client_playback.clear(f)
if self.stream:
self.stream.add(f)
return f return f
def handle_intercept(self, f): def handle_intercept(self, f):
@ -471,33 +468,8 @@ class FlowMaster(controller.Master):
@controller.handler @controller.handler
def tcp_close(self, flow): def tcp_close(self, flow):
self.active_flows.discard(flow) self.active_flows.discard(flow)
if self.stream:
self.stream.add(flow)
self.run_scripts("tcp_close", flow) self.run_scripts("tcp_close", flow)
def shutdown(self): def shutdown(self):
super(FlowMaster, self).shutdown() super(FlowMaster, self).shutdown()
# Add all flows that are still active
if self.stream:
for flow in self.active_flows:
self.stream.add(flow)
self.stop_stream()
self.unload_scripts() self.unload_scripts()
def start_stream(self, fp, filt):
self.stream = io.FilteredFlowWriter(fp, filt)
def stop_stream(self):
self.stream.fo.close()
self.stream = None
def start_stream_to_path(self, path, mode="wb", filt=None):
path = os.path.expanduser(path)
try:
f = open(path, mode)
self.start_stream(f, filt)
except IOError as v:
return str(v)
self.stream_path = path

View File

@ -0,0 +1,46 @@
from __future__ import absolute_import, print_function, division
from .. import tutils, mastertest
import os.path
from mitmproxy.builtins import stream
from mitmproxy.flow import master, FlowReader
from mitmproxy.flow import state
from mitmproxy import options
class TestStream(mastertest.MasterTest):
def test_stream(self):
with tutils.tmpdir() as tdir:
p = os.path.join(tdir, "foo")
def r():
r = FlowReader(open(p, "rb"))
return list(r.stream())
s = state.State()
m = master.FlowMaster(
options.Options(
outfile = (p, "wb")
),
None,
s
)
sa = stream.Stream()
m.addons.add(sa)
f = tutils.tflow(resp=True)
self.invoke(m, "request", f)
self.invoke(m, "response", f)
m.addons.remove(sa)
assert r()[0].response
m.options.outfile = (p, "ab")
m.addons.add(sa)
f = tutils.tflow()
self.invoke(m, "request", f)
m.addons.remove(sa)
assert not r()[1].response

View File

@ -18,15 +18,14 @@ class MasterTest:
l = proxy.Log("connect") l = proxy.Log("connect")
l.reply = mock.MagicMock() l.reply = mock.MagicMock()
master.log(l) master.log(l)
master.clientconnect(f.client_conn) self.invoke(master, "clientconnect", f.client_conn)
master.serverconnect(f.server_conn) self.invoke(master, "clientconnect", f.client_conn)
master.request(f) self.invoke(master, "serverconnect", f.server_conn)
self.invoke(master, "request", f)
if not f.error: if not f.error:
f.response = models.HTTPResponse.wrap(netlib.tutils.tresp(content=content)) f.response = models.HTTPResponse.wrap(netlib.tutils.tresp(content=content))
f.reply.acked = False self.invoke(master, "response", f)
f = master.response(f) self.invoke(master, "clientdisconnect", f)
f.client_conn.reply.acked = False
master.clientdisconnect(f.client_conn)
return f return f
def dummy_cycle(self, master, n, content): def dummy_cycle(self, master, n, content):

View File

@ -4,31 +4,33 @@ from mitmproxy.exceptions import ContentViewException
import netlib.tutils import netlib.tutils
from mitmproxy import dump, flow, models from mitmproxy import dump, flow, models, exceptions
from . import tutils, mastertest from . import tutils, mastertest
import mock import mock
def test_strfuncs(): def test_strfuncs():
o = dump.Options() o = dump.Options(
tfile = StringIO(),
flow_detail = 0,
)
m = dump.DumpMaster(None, o) m = dump.DumpMaster(None, o)
m.outfile = StringIO()
m.o.flow_detail = 0 m.o.flow_detail = 0
m.echo_flow(tutils.tflow()) m.echo_flow(tutils.tflow())
assert not m.outfile.getvalue() assert not o.tfile.getvalue()
m.o.flow_detail = 4 m.o.flow_detail = 4
m.echo_flow(tutils.tflow()) m.echo_flow(tutils.tflow())
assert m.outfile.getvalue() assert o.tfile.getvalue()
m.outfile = StringIO() o.tfile = StringIO()
m.echo_flow(tutils.tflow(resp=True)) m.echo_flow(tutils.tflow(resp=True))
assert "<<" in m.outfile.getvalue() assert "<<" in o.tfile.getvalue()
m.outfile = StringIO() o.tfile = StringIO()
m.echo_flow(tutils.tflow(err=True)) m.echo_flow(tutils.tflow(err=True))
assert "<<" in m.outfile.getvalue() assert "<<" in o.tfile.getvalue()
flow = tutils.tflow() flow = tutils.tflow()
flow.request = netlib.tutils.treq() flow.request = netlib.tutils.treq()
@ -50,25 +52,32 @@ def test_strfuncs():
def test_contentview(get_content_view): def test_contentview(get_content_view):
get_content_view.side_effect = ContentViewException(""), ("x", iter([])) get_content_view.side_effect = ContentViewException(""), ("x", iter([]))
o = dump.Options(flow_detail=4, verbosity=3) o = dump.Options(
m = dump.DumpMaster(None, o, StringIO()) flow_detail=4,
verbosity=3,
tfile=StringIO(),
)
m = dump.DumpMaster(None, o)
m.echo_flow(tutils.tflow()) m.echo_flow(tutils.tflow())
assert "Content viewer failed" in m.outfile.getvalue() assert "Content viewer failed" in m.options.tfile.getvalue()
class TestDumpMaster(mastertest.MasterTest): class TestDumpMaster(mastertest.MasterTest):
def dummy_cycle(self, master, n, content): def dummy_cycle(self, master, n, content):
mastertest.MasterTest.dummy_cycle(self, master, n, content) mastertest.MasterTest.dummy_cycle(self, master, n, content)
return master.outfile.getvalue() return master.options.tfile.getvalue()
def mkmaster(self, filt, **options): def mkmaster(self, filt, **options):
cs = StringIO()
if "verbosity" not in options: if "verbosity" not in options:
options["verbosity"] = 0 options["verbosity"] = 0
if "flow_detail" not in options: if "flow_detail" not in options:
options["flow_detail"] = 0 options["flow_detail"] = 0
o = dump.Options(filtstr=filt, **options) o = dump.Options(
return dump.DumpMaster(None, o, outfile=cs) filtstr=filt,
tfile=StringIO(),
**options
)
return dump.DumpMaster(None, o)
def test_basic(self): def test_basic(self):
for i in (1, 2, 3): for i in (1, 2, 3):
@ -89,31 +98,33 @@ class TestDumpMaster(mastertest.MasterTest):
) )
def test_error(self): def test_error(self):
cs = StringIO() o = dump.Options(
o = dump.Options(flow_detail=1) tfile=StringIO(),
m = dump.DumpMaster(None, o, outfile=cs) flow_detail=1
)
m = dump.DumpMaster(None, o)
f = tutils.tflow(err=True) f = tutils.tflow(err=True)
m.request(f) m.request(f)
assert m.error(f) assert m.error(f)
assert "error" in cs.getvalue() assert "error" in o.tfile.getvalue()
def test_missing_content(self): def test_missing_content(self):
cs = StringIO() o = dump.Options(
o = dump.Options(flow_detail=3) flow_detail=3,
m = dump.DumpMaster(None, o, outfile=cs) tfile=StringIO(),
)
m = dump.DumpMaster(None, o)
f = tutils.tflow() f = tutils.tflow()
f.request.content = None f.request.content = None
m.request(f) m.request(f)
f.response = models.HTTPResponse.wrap(netlib.tutils.tresp()) f.response = models.HTTPResponse.wrap(netlib.tutils.tresp())
f.response.content = None f.response.content = None
m.response(f) m.response(f)
assert "content missing" in cs.getvalue() assert "content missing" in o.tfile.getvalue()
def test_replay(self): def test_replay(self):
cs = StringIO()
o = dump.Options(server_replay=["nonexistent"], kill=True) o = dump.Options(server_replay=["nonexistent"], kill=True)
tutils.raises(dump.DumpError, dump.DumpMaster, None, o, outfile=cs) tutils.raises(dump.DumpError, dump.DumpMaster, None, o)
with tutils.tmpdir() as t: with tutils.tmpdir() as t:
p = os.path.join(t, "rep") p = os.path.join(t, "rep")
@ -122,7 +133,7 @@ class TestDumpMaster(mastertest.MasterTest):
o = dump.Options(server_replay=[p], kill=True) o = dump.Options(server_replay=[p], kill=True)
o.verbosity = 0 o.verbosity = 0
o.flow_detail = 0 o.flow_detail = 0
m = dump.DumpMaster(None, o, outfile=cs) m = dump.DumpMaster(None, o)
self.cycle(m, b"content") self.cycle(m, b"content")
self.cycle(m, b"content") self.cycle(m, b"content")
@ -130,13 +141,13 @@ class TestDumpMaster(mastertest.MasterTest):
o = dump.Options(server_replay=[p], kill=False) o = dump.Options(server_replay=[p], kill=False)
o.verbosity = 0 o.verbosity = 0
o.flow_detail = 0 o.flow_detail = 0
m = dump.DumpMaster(None, o, outfile=cs) m = dump.DumpMaster(None, o)
self.cycle(m, b"nonexistent") self.cycle(m, b"nonexistent")
o = dump.Options(client_replay=[p], kill=False) o = dump.Options(client_replay=[p], kill=False)
o.verbosity = 0 o.verbosity = 0
o.flow_detail = 0 o.flow_detail = 0
m = dump.DumpMaster(None, o, outfile=cs) m = dump.DumpMaster(None, o)
def test_read(self): def test_read(self):
with tutils.tmpdir() as t: with tutils.tmpdir() as t:
@ -172,20 +183,24 @@ class TestDumpMaster(mastertest.MasterTest):
assert len(m.apps.apps) == 1 assert len(m.apps.apps) == 1
def test_replacements(self): def test_replacements(self):
cs = StringIO() o = dump.Options(
o = dump.Options(replacements=[(".*", "content", "foo")]) replacements=[(".*", "content", "foo")],
tfile = StringIO(),
)
o.verbosity = 0 o.verbosity = 0
o.flow_detail = 0 o.flow_detail = 0
m = dump.DumpMaster(None, o, outfile=cs) m = dump.DumpMaster(None, o)
f = self.cycle(m, b"content") f = self.cycle(m, b"content")
assert f.request.content == b"foo" assert f.request.content == b"foo"
def test_setheader(self): def test_setheader(self):
cs = StringIO() o = dump.Options(
o = dump.Options(setheaders=[(".*", "one", "two")]) setheaders=[(".*", "one", "two")],
tfile=StringIO()
)
o.verbosity = 0 o.verbosity = 0
o.flow_detail = 0 o.flow_detail = 0
m = dump.DumpMaster(None, o, outfile=cs) m = dump.DumpMaster(None, o)
f = self.cycle(m, b"content") f = self.cycle(m, b"content")
assert f.request.headers["one"] == "two" assert f.request.headers["one"] == "two"
@ -212,7 +227,7 @@ class TestDumpMaster(mastertest.MasterTest):
def test_write_err(self): def test_write_err(self):
tutils.raises( tutils.raises(
dump.DumpError, exceptions.OptionsError,
self.mkmaster, None, outfile = ("nonexistentdir/foo", "wb") self.mkmaster, None, outfile = ("nonexistentdir/foo", "wb")
) )

View File

@ -1,5 +1,3 @@
import os.path
import mock import mock
import io import io
@ -887,35 +885,6 @@ class TestFlowMaster:
fm.process_new_request(f) fm.process_new_request(f)
assert "killed" in f.error.msg assert "killed" in f.error.msg
def test_stream(self):
with tutils.tmpdir() as tdir:
p = os.path.join(tdir, "foo")
def read():
with open(p, "rb") as f:
r = flow.FlowReader(f)
return list(r.stream())
s = flow.State()
fm = flow.FlowMaster(None, None, s)
f = tutils.tflow(resp=True)
with open(p, "ab") as tmpfile:
fm.start_stream(tmpfile, None)
fm.request(f)
fm.response(f)
fm.stop_stream()
assert read()[0].response
with open(p, "ab") as tmpfile:
f = tutils.tflow()
fm.start_stream(tmpfile, None)
fm.request(f)
fm.shutdown()
assert not read()[1].response
class TestRequest: class TestRequest: