Add script hooks, enable new engine for mitmdump.

This commit is contained in:
Aldo Cortesi 2011-08-03 16:36:20 +12:00
parent f7e4e89b12
commit 179cf75862
9 changed files with 117 additions and 110 deletions

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from libmproxy import script from libmproxy import script
f = script.load_flow() def response(ctx, f):
f.request.headers["newheader"] = ["foo"] ctx.log("processing a response")
script.return_flow(f) f.response.headers["newheader"] = ["foo"]

View File

@ -1,5 +1,5 @@
import sys, os import sys, os
import flow, filt, utils import flow, filt, utils, script
class DumpError(Exception): pass class DumpError(Exception): pass
@ -15,8 +15,6 @@ class Options(object):
"kill", "kill",
"no_server", "no_server",
"refresh_server_playback", "refresh_server_playback",
"request_script",
"response_script",
"rfile", "rfile",
"rheaders", "rheaders",
"server_replay", "server_replay",
@ -68,11 +66,6 @@ class DumpMaster(flow.FlowMaster):
else: else:
self.filt = None self.filt = None
if self.o.response_script:
self.set_response_script(self.o.response_script)
if self.o.request_script:
self.set_request_script(self.o.request_script)
if options.stickycookie: if options.stickycookie:
self.set_stickycookie(options.stickycookie) self.set_stickycookie(options.stickycookie)
@ -109,6 +102,10 @@ class DumpMaster(flow.FlowMaster):
not options.keepserving not options.keepserving
) )
if options.script:
err = self.load_script(options.script)
if err:
raise DumpError(err)
def _readflow(self, path): def _readflow(self, path):
path = os.path.expanduser(path) path = os.path.expanduser(path)
@ -119,20 +116,6 @@ class DumpMaster(flow.FlowMaster):
raise DumpError(v.strerror) raise DumpError(v.strerror)
return flows return flows
def _runscript(self, f, script):
try:
ret = f.run_script(script)
if self.o.verbosity > 0:
print >> self.outfile, ret
except flow.RunException, e:
if e.errout:
eout = "Script output:\n" + self.indent(4, e.errout) + "\n"
else:
eout = ""
raise DumpError(
"%s: %s\n%s"%(script, e.args[0], eout)
)
def add_event(self, e, level="info"): def add_event(self, e, level="info"):
if self.eventlog: if self.eventlog:
print >> self.outfile, e print >> self.outfile, e

View File

@ -3,7 +3,7 @@
with their responses, and provide filtering and interception facilities. with their responses, and provide filtering and interception facilities.
""" """
import subprocess, sys, json, hashlib, Cookie, cookielib import subprocess, sys, json, hashlib, Cookie, cookielib
import proxy, threading, netstring, filt import proxy, threading, netstring, filt, script
import controller, version import controller, version
class RunException(Exception): class RunException(Exception):
@ -191,10 +191,6 @@ class Flow:
f.load_state(state) f.load_state(state)
return f return f
def script_serialize(self):
data = self.get_state()
return json.dumps(data)
@classmethod @classmethod
def script_deserialize(klass, data): def script_deserialize(klass, data):
try: try:
@ -203,41 +199,6 @@ class Flow:
return None return None
return klass.from_state(data) return klass.from_state(data)
def run_script(self, path):
"""
Run a script on a flow.
Returns a (flow, stderr output) tuple, or raises RunException if
there's an error.
"""
self.backup()
data = self.script_serialize()
try:
p = subprocess.Popen(
[path],
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except OSError, e:
raise RunException(e.args[1], None, None)
so, se = p.communicate(data)
if p.returncode:
raise RunException(
"Script returned error code %s"%p.returncode,
p.returncode,
se
)
f = Flow.script_deserialize(so)
if not f:
raise RunException(
"Invalid response from script.",
p.returncode,
se
)
self.load_state(f.get_state())
return se
def get_state(self, nobackup=False): def get_state(self, nobackup=False):
d = dict( d = dict(
request = self.request.get_state() if self.request else None, request = self.request.get_state() if self.request else None,
@ -463,7 +424,7 @@ class FlowMaster(controller.Master):
self.server_playback = None self.server_playback = None
self.client_playback = None self.client_playback = None
self.kill_nonreplay = False self.kill_nonreplay = False
self.plugin = None self.script = None
self.stickycookie_state = False self.stickycookie_state = False
self.stickycookie_txt = None self.stickycookie_txt = None
@ -476,19 +437,26 @@ class FlowMaster(controller.Master):
self.autodecode = False self.autodecode = False
self.refresh_server_playback = False self.refresh_server_playback = False
def _runscript(self, f, script):
#begin nocover
raise NotImplementedError
#end nocover
def add_event(self, e, level="info"): def add_event(self, e, level="info"):
""" """
level: info, error level: info, error
""" """
pass pass
def set_plugin(self, p): def load_script(self, path):
self.plugin = p """
Loads a script. Returns an error description if something went
wrong.
"""
s = script.Script(path, self)
try:
s.load()
except script.ScriptError, v:
return v.args[0]
ret = s.run("start")
if not ret[0] and ret[1]:
return "Error in script start:\n\n" + ret[1][1]
self.script = s
def set_stickycookie(self, txt): def set_stickycookie(self, txt):
if txt: if txt:
@ -620,11 +588,20 @@ class FlowMaster(controller.Master):
rt.start() rt.start()
#end nocover #end nocover
def handle_clientconnect(self, r): def run_script(self, name, *args, **kwargs):
self.add_event("Connect from: %s:%s"%r.address) if self.script:
r.ack() ret = self.script.run(name, *args, **kwargs)
if not ret[0] and ret[1]:
e = "Script error:\n" + ret[1][1]
self.add_event(e, "error")
def handle_clientconnect(self, cc):
self.run_script("clientconnect", cc)
self.add_event("Connect from: %s:%s"%cc.address)
cc.ack()
def handle_clientdisconnect(self, r): def handle_clientdisconnect(self, r):
self.run_script("clientdisconnect", r)
s = "Disconnect from: %s:%s"%r.client_conn.address s = "Disconnect from: %s:%s"%r.client_conn.address
self.add_event(s) self.add_event(s)
if r.client_conn.requestcount: if r.client_conn.requestcount:
@ -638,6 +615,8 @@ class FlowMaster(controller.Master):
def handle_error(self, r): def handle_error(self, r):
f = self.state.add_error(r) f = self.state.add_error(r)
if f:
self.run_script("error", f)
if self.client_playback: if self.client_playback:
self.client_playback.clear(f) self.client_playback.clear(f)
r.ack() r.ack()
@ -645,11 +624,14 @@ class FlowMaster(controller.Master):
def handle_request(self, r): def handle_request(self, r):
f = self.state.add_request(r) f = self.state.add_request(r)
self.run_script("request", f)
self.process_new_request(f) self.process_new_request(f)
return f return f
def handle_response(self, r): def handle_response(self, r):
f = self.state.add_response(r) f = self.state.add_response(r)
if f:
self.run_script("response", f)
if self.client_playback: if self.client_playback:
self.client_playback.clear(f) self.client_playback.clear(f)
if not f: if not f:

View File

@ -8,7 +8,7 @@ class Context:
self.master, self.state = master, state self.master, self.state = master, state
def log(self, *args, **kwargs): def log(self, *args, **kwargs):
self.master.log(*args, **kwargs) self.master.add_event(*args, **kwargs)
class Script: class Script:
@ -32,9 +32,14 @@ class Script:
Raises ScriptError on failure, with argument equal to an error Raises ScriptError on failure, with argument equal to an error
message that may be a formatted traceback. message that may be a formatted traceback.
""" """
path = os.path.expanduser(self.path)
if not os.path.exists(path):
raise ScriptError("No such file: %s"%self.path)
if not os.path.isfile(path):
raise ScriptError("Not a file: %s"%self.path)
ns = {} ns = {}
try: try:
self.mod = execfile(os.path.expanduser(self.path), {}, ns) self.mod = execfile(path, ns, ns)
except Exception, v: except Exception, v:
raise ScriptError(traceback.format_exc(v)) raise ScriptError(traceback.format_exc(v))
self.ns = ns self.ns = ns

20
test/scripts/all.py Normal file
View File

@ -0,0 +1,20 @@
log = []
def clientconnect(ctx, cc):
ctx.log("XCLIENTCONNECT")
log.append("clientconnect")
def request(ctx, r):
ctx.log("XREQUEST")
log.append("request")
def response(ctx, r):
ctx.log("XRESPONSE")
log.append("response")
def clientdisconnect(ctx, cc):
ctx.log("XCLIENTDISCONNECT")
log.append("clientdisconnect")
def error(ctx, cc):
ctx.log("XERROR")
log.append("error")

3
test/scripts/starterr.py Normal file
View File

@ -0,0 +1,3 @@
def start(ctx):
raise ValueError

View File

@ -111,30 +111,22 @@ class uDumpMaster(libpry.AutoTree):
wfile = "nonexistentdir/foo" wfile = "nonexistentdir/foo"
) )
def test_request_script(self): def test_script(self):
ret = self._dummy_cycle(1, None, "", request_script="scripts/a", verbosity=1) ret = self._dummy_cycle(
assert "TESTOK" in ret 1, None, "",
assert "DEBUG" in ret script="scripts/all.py", verbosity=0, eventlog=True
)
assert "XCLIENTCONNECT" in ret
assert "XREQUEST" in ret
assert "XRESPONSE" in ret
assert "XCLIENTDISCONNECT" in ret
libpry.raises( libpry.raises(
dump.DumpError, dump.DumpError,
self._dummy_cycle, 1, None, "", request_script="nonexistent" self._dummy_cycle, 1, None, "", script="nonexistent"
) )
libpry.raises( libpry.raises(
dump.DumpError, dump.DumpError,
self._dummy_cycle, 1, None, "", request_script="scripts/err_return" self._dummy_cycle, 1, None, "", script="starterr.py"
)
def test_response_script(self):
ret = self._dummy_cycle(1, None, "", response_script="scripts/a", verbosity=1)
assert "TESTOK" in ret
assert "DEBUG" in ret
libpry.raises(
dump.DumpError,
self._dummy_cycle, 1, None, "", response_script="nonexistent"
)
libpry.raises(
dump.DumpError,
self._dummy_cycle, 1, None, "", response_script="scripts/err_return"
) )
def test_stickycookie(self): def test_stickycookie(self):

View File

@ -138,15 +138,6 @@ class uFlow(libpry.AutoTree):
assert "DEBUG" == se.strip() assert "DEBUG" == se.strip()
assert f.request.host == "TESTOK" assert f.request.host == "TESTOK"
def test_run_script_err(self):
f = tutils.tflow()
f.response = tutils.tresp()
f.request = f.response.request
libpry.raises("returned error", f.run_script,"scripts/err_return")
libpry.raises("invalid response", f.run_script,"scripts/err_data")
libpry.raises("no such file", f.run_script,"nonexistent")
libpry.raises("permission denied", f.run_script,"scripts/nonexecutable")
def test_match(self): def test_match(self):
f = tutils.tflow() f = tutils.tflow()
f.response = tutils.tresp() f.response = tutils.tresp()
@ -449,13 +440,38 @@ class uSerialize(libpry.AutoTree):
class uFlowMaster(libpry.AutoTree): class uFlowMaster(libpry.AutoTree):
def test_load_script(self):
s = flow.State()
fm = flow.FlowMaster(None, s)
assert not fm.load_script("scripts/a.py")
assert fm.load_script("nonexistent")
assert "ValueError" in fm.load_script("scripts/starterr.py")
def test_script(self):
s = flow.State()
fm = flow.FlowMaster(None, s)
assert not fm.load_script("scripts/all.py")
req = tutils.treq()
fm.handle_clientconnect(req.client_conn)
assert fm.script.ns["log"][-1] == "clientconnect"
f = fm.handle_request(req)
assert fm.script.ns["log"][-1] == "request"
resp = tutils.tresp(req)
fm.handle_response(resp)
assert fm.script.ns["log"][-1] == "response"
dc = proxy.ClientDisconnect(req.client_conn)
fm.handle_clientdisconnect(dc)
assert fm.script.ns["log"][-1] == "clientdisconnect"
err = proxy.Error(f.request, "msg")
fm.handle_error(err)
assert fm.script.ns["log"][-1] == "error"
def test_all(self): def test_all(self):
s = flow.State() s = flow.State()
fm = flow.FlowMaster(None, s) fm = flow.FlowMaster(None, s)
fm.anticache = True fm.anticache = True
fm.anticomp = True fm.anticomp = True
req = tutils.treq() req = tutils.treq()
fm.handle_clientconnect(req.client_conn) fm.handle_clientconnect(req.client_conn)
f = fm.handle_request(req) f = fm.handle_request(req)

View File

@ -27,17 +27,23 @@ class uScript(libpry.AutoTree):
s = script.Script("nonexistent", fm) s = script.Script("nonexistent", fm)
libpry.raises( libpry.raises(
script.ScriptError, "no such file",
s.load s.load
) )
s = script.Script(os.path.join("scripts", "syntaxerr.py"), fm) s = script.Script("scripts", fm)
libpry.raises(
"not a file",
s.load
)
s = script.Script("scripts/syntaxerr.py", fm)
libpry.raises( libpry.raises(
script.ScriptError, script.ScriptError,
s.load s.load
) )
s = script.Script(os.path.join("scripts", "loaderr.py"), fm) s = script.Script("scripts/loaderr.py", fm)
libpry.raises( libpry.raises(
script.ScriptError, script.ScriptError,
s.load s.load