Add a stickyauth option.

This allows us to replay an HTTP Authorization header, in the same way as we
replay cookies using stickycookies. This lets us conveniently get at HTTP Basic
Auth protected resources through the proxy, but is not enough to do the same
for HTTP Digest auth. We'll put that on the todo list.
This commit is contained in:
Aldo Cortesi 2011-03-20 17:31:54 +13:00
parent 4f877cde6a
commit c726519e73
10 changed files with 125 additions and 6 deletions

View File

@ -4,7 +4,7 @@
<li><a href="@!urlTo("interception.html")!@">Interception</a></li> <li><a href="@!urlTo("interception.html")!@">Interception</a></li>
<li><a href="@!urlTo("clientreplay.html")!@">Client-side replay</a></li> <li><a href="@!urlTo("clientreplay.html")!@">Client-side replay</a></li>
<li><a href="@!urlTo("serverreplay.html")!@">Server-side replay</a></li> <li><a href="@!urlTo("serverreplay.html")!@">Server-side replay</a></li>
<li><a href="@!urlTo("stickycookies.html")!@">Sticky cookies</a></li> <li><a href="@!urlTo("sticky.html")!@">Sticky cookies and auth</a></li>
<li><a href="@!urlTo("anticache.html")!@">Anticache</a></li> <li><a href="@!urlTo("anticache.html")!@">Anticache</a></li>
<li><a href="@!urlTo("filters.html")!@">Filter expressions</a></li> <li><a href="@!urlTo("filters.html")!@">Filter expressions</a></li>
<li><a href="@!urlTo("scripts.html")!@">Scripting API</a></li> <li><a href="@!urlTo("scripts.html")!@">Scripting API</a></li>

View File

@ -70,7 +70,7 @@ pages = [
Page("interception.html", "Interception"), Page("interception.html", "Interception"),
Page("clientreplay.html", "Client-side replay"), Page("clientreplay.html", "Client-side replay"),
Page("serverreplay.html", "Server-side replay"), Page("serverreplay.html", "Server-side replay"),
Page("stickycookies.html", "Sticky cookies"), Page("sticky.html", "Sticky cookies and auth"),
Page("anticache.html", "Anticache"), Page("anticache.html", "Anticache"),
Page("filters.html", "Filter expressions"), Page("filters.html", "Filter expressions"),
Page("scripts.html", "External scripts"), Page("scripts.html", "External scripts"),

View File

@ -15,3 +15,10 @@ process once, and simply replay it on startup every time you need to interact
with the secured resources. with the secured resources.
## Sticky auth
The __stickyauth__ option is analagous to the __stickycookie__ option, in that
HTTP __Authorization__ headers are simply replayed to the server once they have
been seen. This is enough to allow you to access a server resource using HTTP
Basic authentication through the proxy. Note that __mitmproxy__ doesn't (yet)
support replay of HTTP Digest authentication.

View File

@ -3,11 +3,17 @@ import optparse
def get_common_options(options): def get_common_options(options):
stickycookie = None stickycookie, stickyauth = None, None
if options.stickycookie_all: if options.stickycookie_all:
stickycookie = ".*" stickycookie = ".*"
elif options.stickycookie_filt: elif options.stickycookie_filt:
stickycookie = options.stickycookie_filt stickycookie = options.stickycookie_filt
if options.stickyauth_all:
stickyauth = ".*"
elif options.stickyauth_filt:
stickyauth = options.stickyauth_filt
return dict( return dict(
verbosity = options.verbose, verbosity = options.verbose,
wfile = options.wfile, wfile = options.wfile,
@ -18,6 +24,7 @@ def get_common_options(options):
rheaders = options.rheaders, rheaders = options.rheaders,
client_replay = options.client_replay, client_replay = options.client_replay,
stickycookie = stickycookie, stickycookie = stickycookie,
stickyauth = stickyauth,
anticache = options.anticache, anticache = options.anticache,
refresh_server_playback = not options.norefresh, refresh_server_playback = not options.norefresh,
) )
@ -69,6 +76,16 @@ def common_options(parser):
action="store", dest="stickycookie_filt", default=None, metavar="FILTER", action="store", dest="stickycookie_filt", default=None, metavar="FILTER",
help="Set sticky cookie filter. Matched against requests." help="Set sticky cookie filter. Matched against requests."
) )
parser.add_option(
"-u",
action="store_true", dest="stickyauth_all", default=None,
help="Set sticky auth for all requests."
)
parser.add_option(
"-U",
action="store", dest="stickyauth_filt", default=None, metavar="FILTER",
help="Set sticky auth filter. Matched against requests."
)
parser.add_option( parser.add_option(
"-v", "-v",
action="count", dest="verbose", default=1, action="count", dest="verbose", default=1,

View File

@ -635,6 +635,10 @@ class StatusBar(WWrap):
r.append("[") r.append("[")
r.append(("statusbar_highlight", "t")) r.append(("statusbar_highlight", "t"))
r.append(":%s]"%self.master.stickycookie_txt) r.append(":%s]"%self.master.stickycookie_txt)
if self.master.stickyauth_txt:
r.append("[")
r.append(("statusbar_highlight", "u"))
r.append(":%s]"%self.master.stickyauth_txt)
opts = [] opts = []
if self.master.anticache: if self.master.anticache:
@ -773,6 +777,7 @@ class Options(object):
"rheaders", "rheaders",
"server_replay", "server_replay",
"stickycookie", "stickycookie",
"stickyauth",
"verbosity", "verbosity",
"wfile", "wfile",
] ]
@ -828,8 +833,10 @@ class ConsoleMaster(flow.FlowMaster):
print >> sys.stderr, "Sticky cookies error:", r print >> sys.stderr, "Sticky cookies error:", r
sys.exit(1) sys.exit(1)
self.stickycookie = None r = self.set_stickyauth(options.stickyauth)
self.stickyhosts = {} if r:
print >> sys.stderr, "Sticky auth error:", r
sys.exit(1)
self.refresh_server_playback = options.refresh_server_playback self.refresh_server_playback = options.refresh_server_playback
self.anticache = options.anticache self.anticache = options.anticache
@ -1136,6 +1143,7 @@ class ConsoleMaster(flow.FlowMaster):
("S", "save all flows matching current limit"), ("S", "save all flows matching current limit"),
("s", "server replay"), ("s", "server replay"),
("t", "set sticky cookie expression"), ("t", "set sticky cookie expression"),
("u", "set sticky auth expression"),
("w", "save this flow"), ("w", "save this flow"),
("page up/down", "page up/down"), ("page up/down", "page up/down"),
("enter", "view connection"), ("enter", "view connection"),
@ -1410,6 +1418,13 @@ class ConsoleMaster(flow.FlowMaster):
self.set_stickycookie self.set_stickycookie
) )
k = None k = None
elif k == "u":
self.prompt(
"Sticky auth filter: ",
self.stickyauth_txt,
self.set_stickyauth
)
k = None
if k: if k:
self.view.keypress(size, k) self.view.keypress(size, k)
except (Stop, KeyboardInterrupt): except (Stop, KeyboardInterrupt):

View File

@ -16,6 +16,7 @@ class Options(object):
"rheaders", "rheaders",
"server_replay", "server_replay",
"stickycookie", "stickycookie",
"stickyauth",
"verbosity", "verbosity",
"wfile", "wfile",
] ]
@ -64,6 +65,9 @@ class DumpMaster(flow.FlowMaster):
if options.stickycookie: if options.stickycookie:
self.set_stickycookie(options.stickycookie) self.set_stickycookie(options.stickycookie)
if options.stickyauth:
self.set_stickyauth(options.stickyauth)
if options.wfile: if options.wfile:
path = os.path.expanduser(options.wfile) path = os.path.expanduser(options.wfile)
try: try:

View File

@ -163,6 +163,21 @@ class StickyCookieState:
l.append(self.jar[i].output(header="").strip()) l.append(self.jar[i].output(header="").strip())
class StickyAuthState:
def __init__(self, flt):
"""
flt: A compiled filter.
"""
self.flt = flt
self.hosts = {}
def handle_request(self, f):
if "authorization" in f.request.headers:
self.hosts[f.request.host] = f.request.headers["authorization"]
elif f.match(self.flt):
if f.request.host in self.hosts:
f.request.headers["authorization"] = self.hosts[f.request.host]
class Flow: class Flow:
def __init__(self, request): def __init__(self, request):
@ -433,6 +448,9 @@ class FlowMaster(controller.Master):
self.stickycookie_state = False self.stickycookie_state = False
self.stickycookie_txt = None self.stickycookie_txt = None
self.stickyauth_state = False
self.stickyauth_txt = None
self.anticache = False self.anticache = False
self.refresh_server_playback = False self.refresh_server_playback = False
@ -458,6 +476,17 @@ class FlowMaster(controller.Master):
self.stickycookie_state = None self.stickycookie_state = None
self.stickycookie_txt = None self.stickycookie_txt = None
def set_stickyauth(self, txt):
if txt:
flt = filt.parse(txt)
if not flt:
return "Invalid filter expression."
self.stickyauth_state = StickyAuthState(flt)
self.stickyauth_txt = txt
else:
self.stickyauth_state = None
self.stickyauth_txt = None
def start_client_playback(self, flows, exit): def start_client_playback(self, flows, exit):
""" """
flows: A list of flows. flows: A list of flows.
@ -516,6 +545,9 @@ class FlowMaster(controller.Master):
def process_new_request(self, f): def process_new_request(self, f):
if self.stickycookie_state: if self.stickycookie_state:
self.stickycookie_state.handle_request(f) self.stickycookie_state.handle_request(f)
if self.stickyauth_state:
self.stickyauth_state.handle_request(f)
if "request" in self.scripts: if "request" in self.scripts:
self._runscript(f, self.scripts["request"]) self._runscript(f, self.scripts["request"])
if self.anticache: if self.anticache:

View File

@ -137,8 +137,9 @@ class Request(controller.Msg):
self.close = False self.close = False
controller.Msg.__init__(self) controller.Msg.__init__(self)
# Have this request's cookies been modified by sticky cookies? # Have this request's cookies been modified by sticky cookies or auth?
self.stickycookie = False self.stickycookie = False
self.stickyauth = False
def anticache(self): def anticache(self):
""" """

View File

@ -34,6 +34,19 @@ class uStickyCookieState(libpry.AutoTree):
assert "cookie" in f.request.headers assert "cookie" in f.request.headers
class uStickyAuthState(libpry.AutoTree):
def test_handle_response(self):
s = flow.StickyAuthState(filt.parse(".*"))
f = tutils.tflow_full()
f.request.headers["authorization"] = ["foo"]
s.handle_request(f)
assert "host" in s.hosts
f = tutils.tflow_full()
s.handle_request(f)
assert f.request.headers["authorization"] == ["foo"]
class uClientPlaybackState(libpry.AutoTree): class uClientPlaybackState(libpry.AutoTree):
def test_tick(self): def test_tick(self):
first = tutils.tflow() first = tutils.tflow()
@ -62,6 +75,9 @@ class uClientPlaybackState(libpry.AutoTree):
fm.state.clear() fm.state.clear()
fm.tick(q) fm.tick(q)
fm.stop_client_playback()
assert not fm.client_playback
class uServerPlaybackState(libpry.AutoTree): class uServerPlaybackState(libpry.AutoTree):
def test_hash(self): def test_hash(self):
@ -441,6 +457,9 @@ class uFlowMaster(libpry.AutoTree):
fm.tick(q) fm.tick(q)
assert controller.exit assert controller.exit
fm.stop_server_playback()
assert not fm.server_playback
def test_stickycookie(self): def test_stickycookie(self):
s = flow.State() s = flow.State()
fm = flow.FlowMaster(None, s) fm = flow.FlowMaster(None, s)
@ -460,9 +479,30 @@ class uFlowMaster(libpry.AutoTree):
fm.handle_request(tf.request) fm.handle_request(tf.request)
assert tf.request.headers["cookie"] == ["foo=bar"] assert tf.request.headers["cookie"] == ["foo=bar"]
def test_stickyauth(self):
s = flow.State()
fm = flow.FlowMaster(None, s)
assert "Invalid" in fm.set_stickyauth("~h")
fm.set_stickyauth(".*")
assert fm.stickyauth_state
fm.set_stickyauth(None)
assert not fm.stickyauth_state
fm.set_stickyauth(".*")
tf = tutils.tflow_full()
tf.request.headers["authorization"] = ["foo"]
fm.handle_request(tf.request)
f = tutils.tflow_full()
assert fm.stickyauth_state.hosts
assert not "authorization" in f.request.headers
fm.handle_request(f.request)
assert f.request.headers["authorization"] == ["foo"]
tests = [ tests = [
uStickyCookieState(), uStickyCookieState(),
uStickyAuthState(),
uServerPlaybackState(), uServerPlaybackState(),
uClientPlaybackState(), uClientPlaybackState(),
uFlow(), uFlow(),

3
todo
View File

@ -20,6 +20,9 @@ Features:
- No-proxy mode, for when we want to just replay without serving as a proxy ourselves. - No-proxy mode, for when we want to just replay without serving as a proxy ourselves.
- Upstream proxy support. - Upstream proxy support.
- Strings view for binary responses. - Strings view for binary responses.
- Support HTTP Digest authentication through the stickyauth option. We'll
have to save the server nonce, and recalculate the hashes for each request.
- Chunked encoding support for requests (we already support it for responses). - Chunked encoding support for requests (we already support it for responses).
- Better manipulation of flows in mitmproxy: copying flows, adding - Better manipulation of flows in mitmproxy: copying flows, adding
comments. comments.