Add an external script API.

External scripts can read a flow, modify it, and then return it to mitmproxy
using a simple API.

The "|" keyboard shortcut within mitmproxy prompts the user for a script.
This commit is contained in:
Aldo Cortesi 2011-01-31 13:26:56 +13:00
parent edb8228dd2
commit b886f808be
8 changed files with 138 additions and 33 deletions

View File

@ -44,46 +44,46 @@ def format_keyvals(lst, key="key", val="text", space=5, indent=0):
return ret return ret
def format_flow(flow, focus, padding=3): def format_flow(f, focus, padding=3):
if not flow.request and not flow.response: if not f.request and not f.response:
txt = [ txt = [
("title", " Connection from %s..."%(flow.connection.address)), ("title", " Connection from %s..."%(f.connection.address)),
] ]
else: else:
txt = [ txt = [
("ack", "!") if flow.intercepting and not flow.request.acked else " ", ("ack", "!") if f.intercepting and not f.request.acked else " ",
("method", flow.request.method), ("method", f.request.method),
" ", " ",
( (
"text" if (flow.response or flow.error) else "title", "text" if (f.response or f.error) else "title",
flow.request.url(), f.request.url(),
), ),
] ]
if flow.response or flow.error or flow.is_replay(): if f.response or f.error or f.is_replay():
txt.append("\n" + " "*(padding+2)) txt.append("\n" + " "*(padding+2))
if flow.is_replay(): if f.is_replay():
txt.append(("method", "[replay] ")) txt.append(("method", "[replay] "))
if not (flow.response or flow.error): if not (f.response or f.error):
txt.append(("text", "waiting for response...")) txt.append(("text", "waiting for response..."))
if flow.response: if f.response:
txt.append( txt.append(
("ack", "!") if flow.intercepting and not flow.response.acked else " " ("ack", "!") if f.intercepting and not f.response.acked else " "
) )
txt.append("<- ") txt.append("<- ")
if flow.response.code in [200, 304]: if f.response.code in [200, 304]:
txt.append(("goodcode", str(flow.response.code))) txt.append(("goodcode", str(f.response.code)))
else: else:
txt.append(("error", str(flow.response.code))) txt.append(("error", str(f.response.code)))
t = flow.response.headers.get("content-type") t = f.response.headers.get("content-type")
if t: if t:
t = t[0].split(";")[0] t = t[0].split(";")[0]
txt.append(("text", " %s"%t)) txt.append(("text", " %s"%t))
if flow.response.content: if f.response.content:
txt.append(", %s"%utils.pretty_size(len(flow.response.content))) txt.append(", %s"%utils.pretty_size(len(f.response.content)))
elif flow.error: elif f.error:
txt.append( txt.append(
("error", flow.error.msg) ("error", f.error.msg)
) )
if focus: if focus:
txt.insert(0, ("focus", ">>" + " "*(padding-2))) txt.insert(0, ("focus", ">>" + " "*(padding-2)))
@ -193,13 +193,13 @@ class ConnectionListView(urwid.ListWalker):
class ConnectionViewHeader(WWrap): class ConnectionViewHeader(WWrap):
def __init__(self, master, flow): def __init__(self, master, f):
self.master, self.flow = master, flow self.master, self.flow = master, f
self.w = urwid.Text(format_flow(flow, False, padding=0)) self.w = urwid.Text(format_flow(f, False, padding=0))
def refresh_connection(self, flow): def refresh_connection(self, f):
if f == self.flow: if f == self.flow:
self.w = urwid.Text(format_flow(flow, False, padding=0)) self.w = urwid.Text(format_flow(f, False, padding=0))
VIEW_BODY_RAW = 0 VIEW_BODY_RAW = 0
@ -520,8 +520,20 @@ class ConnectionView(WWrap):
self.master.prompt("Save response body: ", self.save_body) self.master.prompt("Save response body: ", self.save_body)
elif key == " ": elif key == " ":
self.master.view_next_flow(self.flow) self.master.view_next_flow(self.flow)
elif key == "|":
self.master.path_prompt("Script:", self.run_script)
return key return key
def run_script(self, path):
path = os.path.expanduser(path)
try:
newflow = self.flow.run_script(path)
except flow.RunException, e:
self.master.statusbar.message("Script error: %s"%e)
return
self.flow.load_state(newflow.get_state())
self.master.refresh_connection(self.flow)
class _PathCompleter: class _PathCompleter:
DEFAULTPATH = "/bin:/usr/bin:/usr/local/bin" DEFAULTPATH = "/bin:/usr/bin:/usr/local/bin"
@ -942,6 +954,7 @@ class ConsoleMaster(controller.Master):
("s", "save this flow"), ("s", "save this flow"),
("v", "view contents in external viewer"), ("v", "view contents in external viewer"),
("w", "save request or response body"), ("w", "save request or response body"),
("|", "run script"),
("tab", "toggle response/request view"), ("tab", "toggle response/request view"),
("space", "next flow"), ("space", "next flow"),
] ]

View File

@ -2,9 +2,12 @@
This module provides more sophisticated flow tracking. These match requests This module provides more sophisticated flow tracking. These match requests
with their responses, and provide filtering and interception facilities. with their responses, and provide filtering and interception facilities.
""" """
import subprocess, base64, sys
from contrib import bson from contrib import bson
import proxy, threading import proxy, threading
class RunException(Exception): pass
class ReplayConnection: class ReplayConnection:
pass pass
@ -33,11 +36,39 @@ class Flow:
self.intercepting = False self.intercepting = False
self._backup = None self._backup = None
def run_script(self): def script_serialize(self):
data = self.get_state()
data = bson.dumps(data)
return base64.encodestring(data)
@classmethod
def script_deserialize(klass, data):
data = base64.decodestring(data)
try:
data = bson.loads(data)
# bson.loads doesn't define a particular exception on error...
except Exception:
return None
return klass.from_state(data)
def run_script(self, path):
""" """
Run a script on a flow, returning the modified flow. Run a script on a flow, returning the modified flow.
Raises RunException if there's an error.
""" """
pass data = self.script_serialize()
try:
p = subprocess.Popen([path], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
except OSError, e:
raise RunException(e.args[1])
so, se = p.communicate(data)
if p.returncode:
raise RunException("Script returned error code %s"%p.returncode)
f = Flow.script_deserialize(so)
if not f:
raise RunException("Invalid response from script.")
return f
def dump(self): def dump(self):
data = dict( data = dict(
@ -52,15 +83,18 @@ class Flow:
error = self.error.get_state() if self.error else None, error = self.error.get_state() if self.error else None,
) )
def load_state(self, state):
if state["request"]:
self.request = proxy.Request.from_state(state["request"])
if state["response"]:
self.response = proxy.Response.from_state(self.request, state["response"])
if state["error"]:
self.error = proxy.Error.from_state(state["error"])
@classmethod @classmethod
def from_state(klass, state): def from_state(klass, state):
f = klass(None) f = klass(None)
if state["request"]: f.load_state(state)
f.request = proxy.Request.from_state(state["request"])
if state["response"]:
f.response = proxy.Response.from_state(f.request, state["response"])
if state["error"]:
f.error = proxy.Error.from_state(state["error"])
return f return f
def __eq__(self, other): def __eq__(self, other):

27
libmproxy/script.py Normal file
View File

@ -0,0 +1,27 @@
"""
The mitmproxy scripting interface is simple - a serialized representation
of a flow is passed to the script on stdin, and a possibly modified flow is
then read by mitmproxy from the scripts stdout. This module provides two
convenience functions to make loading and returning data from scripts
simple.
"""
import sys, base64
from contrib import bson
import flow
def load_flow():
"""
Load a flow from the stdin. Returns a Flow object.
"""
data = sys.stdin.read()
return flow.Flow.script_deserialize(data)
def return_flow(f):
"""
Print a flow to stdout.
"""
print >> sys.stdout, f.script_serialize()

8
test/scripts/a Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env python
import sys
sys.path.insert(0, "..")
from libmproxy import script
f = script.load_flow()
f.request.host = "TESTOK"
script.return_flow(f)

2
test/scripts/err_data Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env python
print "NONSENSE"

5
test/scripts/err_return Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env python
import sys
sys.path.insert(0, "..")
sys.exit(1)

View File

View File

@ -3,6 +3,22 @@ import utils
import libpry import libpry
class uFlow(libpry.AutoTree): class uFlow(libpry.AutoTree):
def test_run_script(self):
f = utils.tflow()
f.response = utils.tresp()
f.request = f.response.request
f = f.run_script("scripts/a")
assert f.request.host == "TESTOK"
def test_run_script_err(self):
f = utils.tflow()
f.response = utils.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 = utils.tflow() f = utils.tflow()
f.response = utils.tresp() f.response = utils.tresp()