continue work on the proxyhandler
This commit is contained in:
commit
ea2f17680b
Binary file not shown.
Binary file not shown.
|
@ -1,4 +1,6 @@
|
|||
import Image, cStringIO
|
||||
import cStringIO
|
||||
from PIL import Image
|
||||
|
||||
def response(context, flow):
|
||||
if flow.response.headers["content-type"] == ["image/png"]:
|
||||
s = cStringIO.StringIO(flow.response.content)
|
||||
|
|
|
@ -3,6 +3,8 @@ import flask
|
|||
mapp = flask.Flask(__name__)
|
||||
mapp.debug = True
|
||||
|
||||
def master():
|
||||
return flask.request.environ["mitmproxy.master"]
|
||||
|
||||
@mapp.route("/")
|
||||
def index():
|
||||
|
|
|
@ -271,7 +271,9 @@ def common_options(parser):
|
|||
group.add_argument(
|
||||
"--app-host",
|
||||
action="store", dest="app_host", default=APP_HOST, metavar="host",
|
||||
help="Domain to serve the app from. For transparent mode, use an IP when a DNS entry for the app domain is not present."
|
||||
help="Domain to serve the app from. For transparent mode, use an IP when\
|
||||
a DNS entry for the app domain is not present. Default: %s"%APP_HOST
|
||||
|
||||
)
|
||||
group.add_argument(
|
||||
"--app-port",
|
||||
|
|
|
@ -174,8 +174,7 @@ class StatusBar(common.WWrap):
|
|||
r.append("[%s]"%(":".join(opts)))
|
||||
|
||||
if self.master.scripts:
|
||||
r.append("[script:%s]"%self.master.script.path)
|
||||
|
||||
r.append("[scripts:%s]"%len(self.master.scripts))
|
||||
if self.master.debug:
|
||||
r.append("[lt:%0.3f]"%self.master.looptime)
|
||||
|
||||
|
@ -335,7 +334,7 @@ class Options(object):
|
|||
"no_server",
|
||||
"refresh_server_playback",
|
||||
"rfile",
|
||||
"script",
|
||||
"scripts",
|
||||
"showhost",
|
||||
"replacements",
|
||||
"rheaders",
|
||||
|
@ -410,11 +409,12 @@ class ConsoleMaster(flow.FlowMaster):
|
|||
|
||||
self.debug = options.debug
|
||||
|
||||
if options.script:
|
||||
err = self.load_script(options.script)
|
||||
if err:
|
||||
print >> sys.stderr, "Script load error:", err
|
||||
sys.exit(1)
|
||||
if options.scripts:
|
||||
for i in options.scripts:
|
||||
err = self.load_script(i)
|
||||
if err:
|
||||
print >> sys.stderr, "Script load error:", err
|
||||
sys.exit(1)
|
||||
|
||||
if options.wfile:
|
||||
err = self.start_stream(options.wfile)
|
||||
|
@ -423,7 +423,7 @@ class ConsoleMaster(flow.FlowMaster):
|
|||
sys.exit(1)
|
||||
|
||||
if options.app:
|
||||
self.start_app(self.o.app_host, self.o.app_port, self.o.app_external)
|
||||
self.start_app(self.options.app_host, self.options.app_port, self.options.app_external)
|
||||
|
||||
def start_stream(self, path):
|
||||
path = os.path.expanduser(path)
|
||||
|
@ -434,7 +434,6 @@ class ConsoleMaster(flow.FlowMaster):
|
|||
return str(v)
|
||||
self.stream_path = path
|
||||
|
||||
|
||||
def _run_script_method(self, method, s, f):
|
||||
status, val = s.run(method, f)
|
||||
if val:
|
||||
|
@ -447,7 +446,7 @@ class ConsoleMaster(flow.FlowMaster):
|
|||
if not path:
|
||||
return
|
||||
self.add_event("Running script on flow: %s"%path)
|
||||
ret = self.get_script(path)
|
||||
ret = self.get_script(shlex.split(path, posix=(os.name != "nt")))
|
||||
if ret[0]:
|
||||
self.statusbar.message("Error loading script.")
|
||||
self.add_event("Error loading script:\n%s"%ret[0])
|
||||
|
@ -880,14 +879,21 @@ class ConsoleMaster(flow.FlowMaster):
|
|||
)
|
||||
)
|
||||
elif k == "s":
|
||||
if self.scripts:
|
||||
self.load_script(None)
|
||||
else:
|
||||
self.path_prompt(
|
||||
"Set script: ",
|
||||
self.state.last_script,
|
||||
self.set_script
|
||||
self.view_grideditor(
|
||||
grideditor.ScriptEditor(
|
||||
self,
|
||||
[[i.argv[0]] for i in self.scripts],
|
||||
None
|
||||
)
|
||||
)
|
||||
#if self.scripts:
|
||||
# self.load_script(None)
|
||||
#else:
|
||||
# self.path_prompt(
|
||||
# "Set script: ",
|
||||
# self.state.last_script,
|
||||
# self.set_script
|
||||
# )
|
||||
elif k == "S":
|
||||
if not self.server_playback:
|
||||
self.path_prompt(
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import logging
|
||||
import re, cStringIO, traceback, json
|
||||
import urwid
|
||||
|
||||
try: from PIL import Image
|
||||
except ImportError: import Image
|
||||
|
||||
try: from PIL.ExifTags import TAGS
|
||||
except ImportError: from ExifTags import TAGS
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS
|
||||
|
||||
import lxml.html, lxml.etree
|
||||
import netlib.utils
|
||||
|
@ -19,6 +17,18 @@ try:
|
|||
except ImportError: # pragma nocover
|
||||
pyamf = None
|
||||
|
||||
try:
|
||||
import cssutils
|
||||
except ImportError: # pragma nocover
|
||||
cssutils = None
|
||||
else:
|
||||
cssutils.log.setLevel(logging.CRITICAL)
|
||||
|
||||
cssutils.ser.prefs.keepComments = True
|
||||
cssutils.ser.prefs.omitLastSemicolon = False
|
||||
cssutils.ser.prefs.indentClosingBrace = False
|
||||
cssutils.ser.prefs.validOnly = False
|
||||
|
||||
VIEW_CUTOFF = 1024*50
|
||||
|
||||
|
||||
|
@ -318,7 +328,23 @@ class ViewJavaScript:
|
|||
opts = jsbeautifier.default_options()
|
||||
opts.indent_size = 2
|
||||
res = jsbeautifier.beautify(content[:limit], opts)
|
||||
return "JavaScript", _view_text(res, len(content), limit)
|
||||
return "JavaScript", _view_text(res, len(res), limit)
|
||||
|
||||
class ViewCSS:
|
||||
name = "CSS"
|
||||
prompt = ("css", "c")
|
||||
content_types = [
|
||||
"text/css"
|
||||
]
|
||||
|
||||
def __call__(self, hdrs, content, limit):
|
||||
if cssutils:
|
||||
sheet = cssutils.parseString(content)
|
||||
beautified = sheet.cssText
|
||||
else:
|
||||
beautified = content
|
||||
|
||||
return "CSS", _view_text(beautified, len(beautified), limit)
|
||||
|
||||
|
||||
class ViewImage:
|
||||
|
@ -409,6 +435,7 @@ views = [
|
|||
ViewHTML(),
|
||||
ViewHTMLOutline(),
|
||||
ViewJavaScript(),
|
||||
ViewCSS(),
|
||||
ViewURLEncoded(),
|
||||
ViewMultipart(),
|
||||
ViewImage(),
|
||||
|
|
|
@ -12,6 +12,7 @@ def _mkhelp():
|
|||
("e", "toggle eventlog"),
|
||||
("F", "toggle follow flow list"),
|
||||
("l", "set limit filter pattern"),
|
||||
("/", "same as above"),
|
||||
("L", "load saved flows"),
|
||||
("r", "replay request"),
|
||||
("V", "revert changes to request"),
|
||||
|
@ -244,7 +245,7 @@ class FlowListBox(urwid.ListBox):
|
|||
self.master.clear_flows()
|
||||
elif key == "e":
|
||||
self.master.toggle_eventlog()
|
||||
elif key == "l":
|
||||
elif key == "l" or key == "/":
|
||||
self.master.prompt("Limit: ", self.master.state.limit_txt, self.master.set_limit)
|
||||
elif key == "L":
|
||||
self.master.path_prompt(
|
||||
|
|
|
@ -63,6 +63,8 @@ def _mkhelp():
|
|||
("tab", "toggle request/response view"),
|
||||
("space", "next flow"),
|
||||
("|", "run script on this flow"),
|
||||
("/", "search in response body (case sensitive)"),
|
||||
("n", "repeat previous search"),
|
||||
]
|
||||
text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
|
||||
return text
|
||||
|
@ -85,7 +87,9 @@ class FlowViewHeader(common.WWrap):
|
|||
|
||||
|
||||
class CallbackCache:
|
||||
@utils.LRUCache(200)
|
||||
#commented decorator because it was breaking search functionality (caching after
|
||||
# searches.) If it can be made to only cache the first time, it'd be great.
|
||||
#@utils.LRUCache(200)
|
||||
def _callback(self, method, *args, **kwargs):
|
||||
return getattr(self.obj, method)(*args, **kwargs)
|
||||
|
||||
|
@ -109,8 +113,12 @@ class FlowView(common.WWrap):
|
|||
("options", "o"),
|
||||
("edit raw", "e"),
|
||||
]
|
||||
|
||||
highlight_color = "focusfield"
|
||||
|
||||
def __init__(self, master, state, flow):
|
||||
self.master, self.state, self.flow = master, state, flow
|
||||
self.last_displayed_body = None
|
||||
if self.state.view_flow_mode == common.VIEW_FLOW_RESPONSE:
|
||||
self.view_response()
|
||||
else:
|
||||
|
@ -129,7 +137,8 @@ class FlowView(common.WWrap):
|
|||
limit = sys.maxint
|
||||
else:
|
||||
limit = contentview.VIEW_CUTOFF
|
||||
return cache.callback(
|
||||
|
||||
description, text_objects = cache.callback(
|
||||
self, "_cached_content_view",
|
||||
viewmode,
|
||||
tuple(tuple(i) for i in conn.headers.lst),
|
||||
|
@ -137,49 +146,84 @@ class FlowView(common.WWrap):
|
|||
limit
|
||||
)
|
||||
|
||||
def conn_text(self, conn):
|
||||
txt = common.format_keyvals(
|
||||
return (description, text_objects)
|
||||
|
||||
def cont_view_handle_missing(self, conn, viewmode):
|
||||
if conn.content == flow.CONTENT_MISSING:
|
||||
msg, body = "", [urwid.Text([("error", "[content missing]")])], 0
|
||||
else:
|
||||
msg, body = self.content_view(viewmode, conn)
|
||||
|
||||
return (msg, body)
|
||||
|
||||
def viewmode_get(self, override):
|
||||
return self.state.default_body_view if override is None else override
|
||||
|
||||
def override_get(self):
|
||||
return self.state.get_flow_setting(self.flow,
|
||||
(self.state.view_flow_mode, "prettyview"))
|
||||
|
||||
def conn_text_raw(self, conn):
|
||||
"""
|
||||
Based on a request/response, conn, returns the elements for
|
||||
display.
|
||||
"""
|
||||
headers = common.format_keyvals(
|
||||
[(h+":", v) for (h, v) in conn.headers.lst],
|
||||
key = "header",
|
||||
val = "text"
|
||||
)
|
||||
|
||||
if conn.content is not None:
|
||||
override = self.state.get_flow_setting(
|
||||
self.flow,
|
||||
(self.state.view_flow_mode, "prettyview"),
|
||||
)
|
||||
viewmode = self.state.default_body_view if override is None else override
|
||||
|
||||
if conn.content == flow.CONTENT_MISSING:
|
||||
msg, body = "", [urwid.Text([("error", "[content missing]")])]
|
||||
else:
|
||||
msg, body = self.content_view(viewmode, conn)
|
||||
|
||||
cols = [
|
||||
urwid.Text(
|
||||
[
|
||||
("heading", msg),
|
||||
]
|
||||
)
|
||||
]
|
||||
if override is not None:
|
||||
cols.append(
|
||||
urwid.Text(
|
||||
[
|
||||
" ",
|
||||
('heading', "["),
|
||||
('heading_key', "m"),
|
||||
('heading', (":%s]"%viewmode.name)),
|
||||
],
|
||||
align="right"
|
||||
)
|
||||
)
|
||||
title = urwid.AttrWrap(urwid.Columns(cols), "heading")
|
||||
txt.append(title)
|
||||
txt.extend(body)
|
||||
override = self.override_get()
|
||||
viewmode = self.viewmode_get(override)
|
||||
msg, body = self.cont_view_handle_missing(conn, viewmode)
|
||||
elif conn.content == flow.CONTENT_MISSING:
|
||||
pass
|
||||
return urwid.ListBox(txt)
|
||||
|
||||
return headers, msg, body
|
||||
|
||||
def conn_text_merge(self, headers, msg, body):
|
||||
"""
|
||||
Grabs what is returned by conn_text_raw and merges them all
|
||||
toghether, mainly used by conn_text and search
|
||||
"""
|
||||
|
||||
override = self.override_get()
|
||||
viewmode = self.viewmode_get(override)
|
||||
|
||||
cols = [urwid.Text(
|
||||
[
|
||||
("heading", msg),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
if override is not None:
|
||||
cols.append(urwid.Text([
|
||||
" ",
|
||||
('heading', "["),
|
||||
('heading_key', "m"),
|
||||
('heading', (":%s]"%viewmode.name)),
|
||||
],
|
||||
align="right"
|
||||
)
|
||||
)
|
||||
|
||||
title = urwid.AttrWrap(urwid.Columns(cols), "heading")
|
||||
headers.append(title)
|
||||
headers.extend(body)
|
||||
|
||||
return headers
|
||||
|
||||
def conn_text(self, conn):
|
||||
"""
|
||||
Same as conn_text_raw, but returns result wrapped in a listbox ready for usage.
|
||||
"""
|
||||
headers, msg, body = self.conn_text_raw(conn)
|
||||
merged = self.conn_text_merge(headers, msg, body)
|
||||
|
||||
return urwid.ListBox(merged)
|
||||
|
||||
def _tab(self, content, attr):
|
||||
p = urwid.Text(content)
|
||||
|
@ -215,6 +259,140 @@ class FlowView(common.WWrap):
|
|||
)
|
||||
return f
|
||||
|
||||
def search_wrapped_around(self, last_find_line, last_search_index):
|
||||
"""
|
||||
returns true if search wrapped around the bottom.
|
||||
"""
|
||||
|
||||
current_find_line = self.state.get_flow_setting(self.flow,
|
||||
"last_find_line")
|
||||
current_search_index = self.state.get_flow_setting(self.flow,
|
||||
"last_search_index")
|
||||
|
||||
if current_find_line <= last_find_line:
|
||||
return True
|
||||
elif current_find_line == last_find_line:
|
||||
if current_search_index <= last_search_index:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def search(self, search_string):
|
||||
"""
|
||||
similar to view_response or view_request, but instead of just
|
||||
displaying the conn, it highlights a word that the user is
|
||||
searching for and handles all the logic surrounding that.
|
||||
"""
|
||||
|
||||
if search_string == "":
|
||||
search_string = self.state.get_flow_setting(self.flow,
|
||||
"last_search_string")
|
||||
|
||||
if self.state.view_flow_mode == common.VIEW_FLOW_REQUEST:
|
||||
text = self.flow.request
|
||||
const = common.VIEW_FLOW_REQUEST
|
||||
else:
|
||||
text = self.flow.response
|
||||
const = common.VIEW_FLOW_RESPONSE
|
||||
if not self.flow.response:
|
||||
return "no response to search in"
|
||||
|
||||
last_find_line = self.state.get_flow_setting(self.flow,
|
||||
"last_find_line")
|
||||
last_search_index = self.state.get_flow_setting(self.flow,
|
||||
"last_search_index")
|
||||
|
||||
# generate the body, highlight the words and get focus
|
||||
headers, msg, body = self.conn_text_raw(text)
|
||||
body, focus_position = self.search_highlight_text(body, search_string)
|
||||
|
||||
if focus_position == None:
|
||||
# no results found.
|
||||
return "no matches for '%s'" % search_string
|
||||
|
||||
# UI stuff.
|
||||
merged = self.conn_text_merge(headers, msg, body)
|
||||
list_box = urwid.ListBox(merged)
|
||||
list_box.set_focus(focus_position + 2)
|
||||
self.w = self.wrap_body(const, list_box)
|
||||
self.master.statusbar.redraw()
|
||||
|
||||
self.last_displayed_body = list_box
|
||||
|
||||
if self.search_wrapped_around(last_find_line, last_search_index):
|
||||
return "search hit BOTTOM, continuing at TOP"
|
||||
|
||||
def search_get_start(self, search_string):
|
||||
start_line = 0
|
||||
start_index = 0
|
||||
last_search_string = self.state.get_flow_setting(self.flow, "last_search_string")
|
||||
if search_string == last_search_string:
|
||||
start_line = self.state.get_flow_setting(self.flow, "last_find_line")
|
||||
start_index = self.state.get_flow_setting(self.flow,
|
||||
"last_search_index")
|
||||
|
||||
if start_index == None:
|
||||
start_index = 0
|
||||
else:
|
||||
start_index += len(search_string)
|
||||
|
||||
if start_line == None:
|
||||
start_line = 0
|
||||
|
||||
else:
|
||||
self.state.add_flow_setting(self.flow, "last_search_string",
|
||||
search_string)
|
||||
|
||||
return (start_line, start_index)
|
||||
|
||||
def search_highlight_text(self, text_objects, search_string, looping = False):
|
||||
start_line, start_index = self.search_get_start(search_string)
|
||||
i = start_line
|
||||
|
||||
found = False
|
||||
for text_object in text_objects[start_line:]:
|
||||
if i != start_line:
|
||||
start_index = 0
|
||||
|
||||
text, style = text_object.get_text()
|
||||
|
||||
find_index = text.find(search_string, start_index)
|
||||
if find_index != -1:
|
||||
before = text[:find_index]
|
||||
after = text[find_index+len(search_string):]
|
||||
new_text = urwid.Text(
|
||||
[
|
||||
before,
|
||||
(self.highlight_color, search_string),
|
||||
after,
|
||||
]
|
||||
)
|
||||
|
||||
self.state.add_flow_setting(self.flow, "last_search_index",
|
||||
find_index)
|
||||
self.state.add_flow_setting(self.flow, "last_find_line", i)
|
||||
|
||||
text_objects[i] = new_text
|
||||
|
||||
found = True
|
||||
|
||||
break
|
||||
|
||||
i += 1
|
||||
|
||||
if found:
|
||||
focus_pos = i
|
||||
else :
|
||||
# loop from the beginning, but not forever.
|
||||
if (start_line == 0 and start_index == 0) or looping:
|
||||
focus_pos = None
|
||||
else:
|
||||
self.state.add_flow_setting(self.flow, "last_search_index", 0)
|
||||
self.state.add_flow_setting(self.flow, "last_find_line", 0)
|
||||
text_objects, focus_pos = self.search_highlight_text(text_objects, search_string, True)
|
||||
|
||||
return text_objects, focus_pos
|
||||
|
||||
def view_request(self):
|
||||
self.state.view_flow_mode = common.VIEW_FLOW_REQUEST
|
||||
body = self.conn_text(self.flow.request)
|
||||
|
@ -574,6 +752,20 @@ class FlowView(common.WWrap):
|
|||
conn
|
||||
)
|
||||
self.master.refresh_flow(self.flow)
|
||||
elif key == "/":
|
||||
last_search_string = self.state.get_flow_setting(self.flow, "last_search_string")
|
||||
search_prompt = "Search body ["+last_search_string+"]: " if last_search_string else "Search body: "
|
||||
self.master.prompt(search_prompt,
|
||||
None,
|
||||
self.search)
|
||||
elif key == "n":
|
||||
last_search_string = self.state.get_flow_setting(self.flow, "last_search_string")
|
||||
if last_search_string:
|
||||
message = self.search(last_search_string)
|
||||
if message:
|
||||
self.master.statusbar.message(message)
|
||||
else:
|
||||
self.master.statusbar.message("no previous searches have been made")
|
||||
else:
|
||||
return key
|
||||
|
||||
|
|
|
@ -482,3 +482,12 @@ class PathEditor(GridEditor):
|
|||
columns = 1
|
||||
headings = ("Component",)
|
||||
|
||||
|
||||
class ScriptEditor(GridEditor):
|
||||
title = "Editing scripts"
|
||||
columns = 1
|
||||
headings = ("Path",)
|
||||
def is_error(self, col, val):
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -61,6 +61,10 @@ class HelpView(urwid.ListBox):
|
|||
common.highlight_key("json", "s") +
|
||||
[("text", ": JSON")]
|
||||
),
|
||||
(None,
|
||||
common.highlight_key("css", "c") +
|
||||
[("text", ": CSS")]
|
||||
),
|
||||
(None,
|
||||
common.highlight_key("urlencoded", "u") +
|
||||
[("text", ": URL-encoded data")]
|
||||
|
|
|
@ -39,13 +39,13 @@ class Channel:
|
|||
def __init__(self, q):
|
||||
self.q = q
|
||||
|
||||
def ask(self, m):
|
||||
def ask(self, mtype, m):
|
||||
"""
|
||||
Decorate a message with a reply attribute, and send it to the
|
||||
master. then wait for a response.
|
||||
"""
|
||||
m.reply = Reply(m)
|
||||
self.q.put(m)
|
||||
self.q.put((mtype, m))
|
||||
while not should_exit:
|
||||
try:
|
||||
# The timeout is here so we can handle a should_exit event.
|
||||
|
@ -54,13 +54,13 @@ class Channel:
|
|||
continue
|
||||
return g
|
||||
|
||||
def tell(self, m):
|
||||
def tell(self, mtype, m):
|
||||
"""
|
||||
Decorate a message with a dummy reply attribute, send it to the
|
||||
master, then return immediately.
|
||||
"""
|
||||
m.reply = DummyReply()
|
||||
self.q.put(m)
|
||||
self.q.put((mtype, m))
|
||||
|
||||
|
||||
class Slave(threading.Thread):
|
||||
|
@ -98,7 +98,7 @@ class Master:
|
|||
while True:
|
||||
# Small timeout to prevent pegging the CPU
|
||||
msg = q.get(timeout=0.01)
|
||||
self.handle(msg)
|
||||
self.handle(*msg)
|
||||
changed = True
|
||||
except Queue.Empty:
|
||||
pass
|
||||
|
@ -112,13 +112,13 @@ class Master:
|
|||
self.tick(self.masterq)
|
||||
self.shutdown()
|
||||
|
||||
def handle(self, msg):
|
||||
c = "handle_" + msg.__class__.__name__.lower()
|
||||
def handle(self, mtype, obj):
|
||||
c = "handle_" + mtype
|
||||
m = getattr(self, c, None)
|
||||
if m:
|
||||
m(msg)
|
||||
m(obj)
|
||||
else:
|
||||
msg.reply()
|
||||
obj.reply()
|
||||
|
||||
def shutdown(self):
|
||||
global should_exit
|
||||
|
|
|
@ -6,7 +6,7 @@ import hashlib, Cookie, cookielib, copy, re, urlparse, os, threading
|
|||
import time, urllib
|
||||
import tnetstring, filt, script, utils, encoding, proxy
|
||||
from email.utils import parsedate_tz, formatdate, mktime_tz
|
||||
from netlib import odict, http, certutils
|
||||
from netlib import odict, http, certutils, wsgi
|
||||
import controller, version
|
||||
import app
|
||||
|
||||
|
@ -17,6 +17,28 @@ ODict = odict.ODict
|
|||
ODictCaseless = odict.ODictCaseless
|
||||
|
||||
|
||||
class AppRegistry:
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
|
||||
def add(self, app, domain, port):
|
||||
"""
|
||||
Add a WSGI app to the registry, to be served for requests to the
|
||||
specified domain, on the specified port.
|
||||
"""
|
||||
self.apps[(domain, port)] = wsgi.WSGIAdaptor(app, domain, port, version.NAMEVERSION)
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Returns an WSGIAdaptor instance if request matches an app, or None.
|
||||
"""
|
||||
if (request.host, request.port) in self.apps:
|
||||
return self.apps[(request.host, request.port)]
|
||||
if "host" in request.headers:
|
||||
host = request.headers["host"][0]
|
||||
return self.apps.get((host, request.port), None)
|
||||
|
||||
|
||||
class ReplaceHooks:
|
||||
def __init__(self):
|
||||
self.lst = []
|
||||
|
@ -289,8 +311,10 @@ class Request(HTTPMsg):
|
|||
|
||||
"""
|
||||
def __init__(
|
||||
self, client_conn, httpversion, host, port, scheme, method, path, headers, content, timestamp_start=None,
|
||||
timestamp_end=None, tcp_setup_timestamp=None, ssl_setup_timestamp=None, ip=None):
|
||||
self, client_conn, httpversion, host, port,
|
||||
scheme, method, path, headers, content, timestamp_start=None,
|
||||
timestamp_end=None, tcp_setup_timestamp=None,
|
||||
ssl_setup_timestamp=None, ip=None):
|
||||
assert isinstance(headers, ODictCaseless)
|
||||
self.client_conn = client_conn
|
||||
self.httpversion = httpversion
|
||||
|
@ -307,6 +331,15 @@ class Request(HTTPMsg):
|
|||
self.stickycookie = False
|
||||
self.stickyauth = False
|
||||
|
||||
# Live attributes - not serialized
|
||||
self.wfile, self.rfile = None, None
|
||||
|
||||
def set_live(self, rfile, wfile):
|
||||
self.wfile, self.rfile = wfile, rfile
|
||||
|
||||
def is_live(self):
|
||||
return bool(self.wfile)
|
||||
|
||||
def anticache(self):
|
||||
"""
|
||||
Modifies this request to remove headers that might produce a cached
|
||||
|
@ -1372,17 +1405,16 @@ class FlowMaster(controller.Master):
|
|||
self.setheaders = SetHeaders()
|
||||
|
||||
self.stream = None
|
||||
app.mapp.config["PMASTER"] = self
|
||||
self.apps = AppRegistry()
|
||||
|
||||
def start_app(self, host, port, external):
|
||||
if not external:
|
||||
self.server.apps.add(
|
||||
self.apps.add(
|
||||
app.mapp,
|
||||
host,
|
||||
port
|
||||
)
|
||||
else:
|
||||
print host
|
||||
threading.Thread(target=app.mapp.run,kwargs={
|
||||
"use_reloader": False,
|
||||
"host": host,
|
||||
|
@ -1430,7 +1462,7 @@ class FlowMaster(controller.Master):
|
|||
def run_script_hook(self, name, *args, **kwargs):
|
||||
for script in self.scripts:
|
||||
self.run_single_script_hook(script, name, *args, **kwargs)
|
||||
|
||||
|
||||
def set_stickycookie(self, txt):
|
||||
if txt:
|
||||
flt = filt.parse(txt)
|
||||
|
@ -1589,9 +1621,11 @@ class FlowMaster(controller.Master):
|
|||
r.reply()
|
||||
|
||||
def handle_serverconnection(self, sc):
|
||||
# To unify the mitmproxy script API, we call the script hook "serverconnect" rather than "serverconnection".
|
||||
# As things are handled differently in libmproxy (ClientConnect + ClientDisconnect vs ServerConnection class),
|
||||
# there is no "serverdisonnect" event at the moment.
|
||||
# To unify the mitmproxy script API, we call the script hook
|
||||
# "serverconnect" rather than "serverconnection". As things are handled
|
||||
# differently in libmproxy (ClientConnect + ClientDisconnect vs
|
||||
# ServerConnection class), there is no "serverdisonnect" event at the
|
||||
# moment.
|
||||
self.run_script_hook("serverconnect", sc)
|
||||
sc.reply()
|
||||
|
||||
|
@ -1605,6 +1639,14 @@ class FlowMaster(controller.Master):
|
|||
return f
|
||||
|
||||
def handle_request(self, r):
|
||||
if r.is_live():
|
||||
app = self.apps.get(r)
|
||||
if app:
|
||||
err = app.serve(r, r.wfile, **{"mitmproxy.master": self})
|
||||
if err:
|
||||
self.add_event("Error in wsgi app. %s"%err, "error")
|
||||
r.reply(proxy.KILL)
|
||||
return
|
||||
f = self.state.add_request(r)
|
||||
self.replacehooks.run(f)
|
||||
self.setheaders.run(f)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from libmproxy.proxy import ProxyError, ConnectionHandler
|
||||
from netlib import http
|
||||
|
||||
|
||||
def handle_messages(conntype, connection_handler):
|
||||
handler = None
|
||||
if conntype == "http":
|
||||
|
@ -11,10 +12,15 @@ def handle_messages(conntype, connection_handler):
|
|||
return handler.handle_messages()
|
||||
|
||||
|
||||
class ConnectionTypeChange(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProtocolHandler(object):
|
||||
def __init__(self, c):
|
||||
self.c = c
|
||||
|
||||
|
||||
class HTTPHandler(ProtocolHandler):
|
||||
|
||||
def handle_messages(self):
|
||||
|
@ -35,33 +41,34 @@ class HTTPHandler(ProtocolHandler):
|
|||
method, path, httpversion = http.parse_init(request_line)
|
||||
headers = self.read_headers(authenticate=True)
|
||||
|
||||
if self.mode == "regular":
|
||||
if self.c.mode == "regular":
|
||||
if method == "CONNECT":
|
||||
r = http.parse_init_connect(request_line)
|
||||
if not r:
|
||||
raise ProxyError(400, "Bad HTTP request line: %s"%repr(request_line))
|
||||
host, port, _ = r
|
||||
if self.config.forward_proxy:
|
||||
self.server_conn.wfile.write(request_line)
|
||||
if self.c.config.forward_proxy:
|
||||
#FIXME: Treat as request, no custom handling
|
||||
self.c.server_conn.wfile.write(request_line)
|
||||
for key, value in headers.items():
|
||||
self.server_conn.wfile.write("%s: %s\r\n"%(key, value))
|
||||
self.server_conn.wfile.write("\r\n")
|
||||
self.c.server_conn.wfile.write("%s: %s\r\n"%(key, value))
|
||||
self.c.server_conn.wfile.write("\r\n")
|
||||
else:
|
||||
self.server_address = (host, port)
|
||||
self.establish_server_connection()
|
||||
self.c.server_address = (host, port)
|
||||
self.c.establish_server_connection()
|
||||
|
||||
self.handle_ssl()
|
||||
self.mode = "transparent"
|
||||
return
|
||||
self.c.handle_ssl()
|
||||
self.c.determine_conntype("transparent", host, port)
|
||||
raise ConnectionTypeChange
|
||||
else:
|
||||
r = http.parse_init_proxy(request_line)
|
||||
if not r:
|
||||
raise ProxyError(400, "Bad HTTP request line: %s"%repr(request_line))
|
||||
method, scheme, host, port, path, httpversion = r
|
||||
if not self.config.forward_proxy:
|
||||
if (not self.server_conn) or (self.server_address != (host, port)):
|
||||
self.server_address = (host, port)
|
||||
self.establish_server_connection()
|
||||
if not self.c.config.forward_proxy:
|
||||
if (not self.c.server_conn) or (self.c.server_address != (host, port)):
|
||||
self.c.server_address = (host, port)
|
||||
self.c.establish_server_connection()
|
||||
|
||||
def get_line(self, fp):
|
||||
"""
|
||||
|
@ -73,16 +80,16 @@ class HTTPHandler(ProtocolHandler):
|
|||
return line
|
||||
|
||||
def read_headers(self, authenticate=False):
|
||||
headers = http.read_headers(self.client_conn.rfile)
|
||||
headers = http.read_headers(self.c.client_conn.rfile)
|
||||
if headers is None:
|
||||
raise ProxyError(400, "Invalid headers")
|
||||
if authenticate and self.config.authenticator:
|
||||
if self.config.authenticator.authenticate(headers):
|
||||
self.config.authenticator.clean(headers)
|
||||
if authenticate and self.c.config.authenticator:
|
||||
if self.c.config.authenticator.authenticate(headers):
|
||||
self.c.config.authenticator.clean(headers)
|
||||
else:
|
||||
raise ProxyError(
|
||||
407,
|
||||
"Proxy Authentication Required",
|
||||
self.config.authenticator.auth_challenge_headers()
|
||||
self.c.config.authenticator.auth_challenge_headers()
|
||||
)
|
||||
return headers
|
|
@ -2,10 +2,12 @@ import sys, os, string, socket, time
|
|||
import shutil, tempfile, threading
|
||||
import SocketServer
|
||||
from OpenSSL import SSL
|
||||
from netlib import odict, tcp, http, wsgi, certutils, http_status, http_auth
|
||||
from netlib import odict, tcp, http, certutils, http_status, http_auth
|
||||
import utils, flow, version, platform, controller, protocol
|
||||
|
||||
|
||||
TRANSPARENT_SSL_PORTS = [443, 8443]
|
||||
|
||||
KILL = 0
|
||||
|
||||
|
||||
|
@ -17,11 +19,6 @@ class ProxyError(Exception):
|
|||
return "ProxyError(%s, %s)"%(self.code, self.msg)
|
||||
|
||||
|
||||
class Log:
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class ProxyConfig:
|
||||
def __init__(self, certfile = None, cacert = None, clientcerts = None, no_upstream_cert=False, body_size_limit = None, reverse_proxy=None, forward_proxy=None, transparent_proxy=None, authenticator=None):
|
||||
self.certfile = certfile
|
||||
|
@ -97,34 +94,10 @@ class RequestReplayThread(threading.Thread):
|
|||
self.flow.request, httpversion, code, msg, headers, content, server.cert,
|
||||
server.rfile.first_byte_timestamp
|
||||
)
|
||||
self.channel.ask(response)
|
||||
self.channel.ask("response", response)
|
||||
except (ProxyError, http.HttpError, tcp.NetLibError), v:
|
||||
err = flow.Error(self.flow.request, str(v))
|
||||
self.channel.ask(err)
|
||||
|
||||
|
||||
class HandleSNI:
|
||||
def __init__(self, handler, cert, key):
|
||||
self.handler = handler
|
||||
self.cert, self.key = cert, key
|
||||
|
||||
def __call__(self, connection):
|
||||
try:
|
||||
sn = connection.get_servername()
|
||||
if sn:
|
||||
self.handler.sni = sn.decode("utf8").encode("idna")
|
||||
self.handler.establish_server_connection()
|
||||
self.handler.handle_ssl()
|
||||
new_context = SSL.Context(SSL.TLSv1_METHOD)
|
||||
new_context.use_privatekey_file(self.key)
|
||||
new_context.use_certificate(self.cert.x509)
|
||||
connection.set_context(new_context)
|
||||
# FIXME: How does that work?
|
||||
# An unhandled exception in this method will core dump PyOpenSSL, so
|
||||
# make dang sure it doesn't happen.
|
||||
except Exception, e: # pragma: no cover
|
||||
pass
|
||||
|
||||
self.channel.ask("error", err)
|
||||
|
||||
class ConnectionHandler:
|
||||
def __init__(self, config, client_connection, client_address, server, channel, server_version):
|
||||
|
@ -145,46 +118,44 @@ class ConnectionHandler:
|
|||
def del_server_connection(self):
|
||||
if self.server_conn:
|
||||
self.server_conn.terminate()
|
||||
self.channel.tell("serverdisconnect", self)
|
||||
self.server_conn = None
|
||||
self.sni = None
|
||||
|
||||
def handle(self):
|
||||
cc = flow.ClientConnect(self.client_address)
|
||||
self.log(cc, "connect")
|
||||
self.channel.ask(cc)
|
||||
self.log("connect")
|
||||
self.channel.ask("clientconnect", self)
|
||||
|
||||
# Can we already identify the target server and connect to it?
|
||||
if self.config.forward_proxy:
|
||||
self.server_address = self.config.forward_proxy
|
||||
self.server_address = self.config.forward_proxy[1:]
|
||||
else:
|
||||
if self.config.reverse_proxy:
|
||||
self.server_address = self.config.reverse_proxy
|
||||
self.server_address = self.config.reverse_proxy[1:]
|
||||
elif self.config.transparent_proxy:
|
||||
self.server_address = self.config.transparent_proxy["resolver"].original_addr(self.connection)
|
||||
if not self.server_address:
|
||||
raise ProxyError(502, "Transparent mode failure: could not resolve original destination.")
|
||||
self.log(cc, "transparent to %s:%s"%self.server_address)
|
||||
self.log("transparent to %s:%s"%self.server_address)
|
||||
|
||||
if self.server_address:
|
||||
self.establish_server_connection()
|
||||
self.handle_ssl()
|
||||
|
||||
self.determine_conntype(self.mode)
|
||||
self.determine_conntype(self.mode, *self.server_address)
|
||||
|
||||
while not cc.close:
|
||||
protocol.handle_messages(self.conntype, self)
|
||||
while not self.close:
|
||||
try:
|
||||
protocol.handle_messages(self.conntype, self)
|
||||
except protocol.ConnectionTypeChange:
|
||||
continue
|
||||
|
||||
cc.close = True
|
||||
self.del_server_connection()
|
||||
|
||||
cd = flow.ClientDisconnect(cc)
|
||||
self.log(
|
||||
cc, "disconnect",
|
||||
[
|
||||
"handled %s requests"%cc.requestcount]
|
||||
)
|
||||
self.channel.tell(cd)
|
||||
self.log("disconnect")
|
||||
self.channel.tell("clientdisconnect", self)
|
||||
|
||||
def determine_conntype(self, mode):
|
||||
def determine_conntype(self, mode, host, port):
|
||||
#TODO: Add ruleset to select correct protocol depending on mode/target port etc.
|
||||
self.conntype = "http"
|
||||
|
||||
|
@ -195,14 +166,15 @@ class ConnectionHandler:
|
|||
"""
|
||||
self.del_server_connection()
|
||||
self.server_conn = ServerConnection(self.config, *self.server_address, self.sni)
|
||||
self.channel.tell("serverconnect", self)
|
||||
|
||||
def handle_ssl(self):
|
||||
if self.config.transparent_proxy:
|
||||
client_ssl, server_ssl = (self.server_address[1] in self.config.transparent_proxy["sslports"])
|
||||
elif self.config.reverse_proxy:
|
||||
client_ssl, server_ssl = self.config.reverse_proxy[0] == "https"
|
||||
# FIXME: Make protocol generic (as with transparent proxies)
|
||||
# FIXME: Add SSL-terminating capatbility (SSL -> mitmproxy -> plain and vice versa)
|
||||
client_ssl, server_ssl = (self.config.reverse_proxy[0] == "https")
|
||||
# TODO: Make protocol generic (as with transparent proxies)
|
||||
# TODO: Add SSL-terminating capatbility (SSL -> mitmproxy -> plain and vice versa)
|
||||
else:
|
||||
client_ssl, server_ssl = True # In regular mode, this function will only be called on HTTP CONNECT
|
||||
|
||||
|
@ -211,37 +183,58 @@ class ConnectionHandler:
|
|||
if server_ssl and not self.server_conn.ssl_established:
|
||||
self.server_conn.establish_ssl()
|
||||
if client_ssl and not self.client_conn.ssl_established:
|
||||
dummycert = self.find_cert(self.client_conn, *self.server_address)
|
||||
sni = HandleSNI(
|
||||
self, dummycert, self.config.certfile or self.config.cacert
|
||||
)
|
||||
dummycert = self.find_cert()
|
||||
self.client_conn.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=self.handle_sni)
|
||||
|
||||
def log(self, msg, subs=()):
|
||||
msg = [
|
||||
"%s:%s: "%self.client_address + msg
|
||||
"%s:%s: "%(self.client_address, msg)
|
||||
]
|
||||
for i in subs:
|
||||
msg.append(" -> "+i)
|
||||
msg = "\n".join(msg)
|
||||
l = Log(msg)
|
||||
self.channel.tell(l)
|
||||
self.channel.tell("log", msg)
|
||||
|
||||
def find_cert(self, cc, host, port, sni=None):
|
||||
def find_cert(self):
|
||||
if self.config.certfile:
|
||||
with open(self.config.certfile, "rb") as f:
|
||||
return certutils.SSLCert.from_pem(f.read())
|
||||
else:
|
||||
host = self.server_address[0]
|
||||
sans = []
|
||||
if not self.config.no_upstream_cert:
|
||||
conn = self.get_server_connection(cc, "https", host, port, sni)
|
||||
sans = conn.cert.altnames
|
||||
if conn.cert.cn:
|
||||
host = conn.cert.cn.decode("utf8").encode("idna")
|
||||
if not self.config.no_upstream_cert or not self.server_conn.ssl_established:
|
||||
upstream_cert = self.server_conn.cert
|
||||
if upstream_cert.cn:
|
||||
host = upstream_cert.cn.decode("utf8").encode("idna")
|
||||
sans = upstream_cert.altnames
|
||||
|
||||
ret = self.config.certstore.get_cert(host, sans, self.config.cacert)
|
||||
if not ret:
|
||||
raise ProxyError(502, "Unable to generate dummy cert.")
|
||||
return ret
|
||||
|
||||
def handle_sni(self, connection):
|
||||
"""
|
||||
This callback gets called during the SSL handshake with the client.
|
||||
The client has just sent the Sever Name Indication (SNI). We now connect upstream to
|
||||
figure out which certificate needs to be served.
|
||||
"""
|
||||
try:
|
||||
sn = connection.get_servername()
|
||||
if sn and sn != self.sni:
|
||||
self.sni = sn.decode("utf8").encode("idna")
|
||||
self.establish_server_connection() # reconnect to upstream server with SNI
|
||||
self.handle_ssl() # establish SSL with upstream
|
||||
# Now, change client context to reflect changed certificate:
|
||||
new_context = SSL.Context(SSL.TLSv1_METHOD)
|
||||
new_context.use_privatekey_file(self.config.certfile or self.config.cacert)
|
||||
dummycert = self.find_cert()
|
||||
new_context.use_certificate(dummycert.x509)
|
||||
connection.set_context(new_context)
|
||||
# An unhandled exception in this method will core dump PyOpenSSL, so
|
||||
# make dang sure it doesn't happen.
|
||||
except Exception, e: # pragma: no cover
|
||||
pass
|
||||
|
||||
class ProxyServerError(Exception): pass
|
||||
|
||||
|
@ -260,7 +253,6 @@ class ProxyServer(tcp.TCPServer):
|
|||
except socket.error, v:
|
||||
raise ProxyServerError('Error starting proxy server: ' + v.strerror)
|
||||
self.channel = None
|
||||
self.apps = AppRegistry()
|
||||
|
||||
def start_slave(self, klass, channel):
|
||||
slave = klass(channel, self)
|
||||
|
@ -275,28 +267,6 @@ class ProxyServer(tcp.TCPServer):
|
|||
h.finish()
|
||||
|
||||
|
||||
class AppRegistry:
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
|
||||
def add(self, app, domain, port):
|
||||
"""
|
||||
Add a WSGI app to the registry, to be served for requests to the
|
||||
specified domain, on the specified port.
|
||||
"""
|
||||
self.apps[(domain, port)] = wsgi.WSGIAdaptor(app, domain, port, version.NAMEVERSION)
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Returns an WSGIAdaptor instance if request matches an app, or None.
|
||||
"""
|
||||
if (request.host, request.port) in self.apps:
|
||||
return self.apps[(request.host, request.port)]
|
||||
if "host" in request.headers:
|
||||
host = request.headers["host"][0]
|
||||
return self.apps.get((host, request.port), None)
|
||||
|
||||
|
||||
class DummyServer:
|
||||
bound = False
|
||||
def __init__(self, config):
|
||||
|
@ -324,7 +294,6 @@ def certificate_option_group(parser):
|
|||
)
|
||||
|
||||
|
||||
TRANSPARENT_SSL_PORTS = [443, 8443]
|
||||
|
||||
def process_proxy_options(parser, options):
|
||||
if options.cert:
|
||||
|
@ -367,7 +336,9 @@ def process_proxy_options(parser, options):
|
|||
if options.clientcerts:
|
||||
options.clientcerts = os.path.expanduser(options.clientcerts)
|
||||
if not os.path.exists(options.clientcerts) or not os.path.isdir(options.clientcerts):
|
||||
return parser.error("Client certificate directory does not exist or is not a directory: %s"%options.clientcerts)
|
||||
return parser.error(
|
||||
"Client certificate directory does not exist or is not a directory: %s"%options.clientcerts
|
||||
)
|
||||
|
||||
if (options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd):
|
||||
if options.auth_singleuser:
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -2,67 +2,38 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
|
||||
<title>mitmproxy</title>
|
||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/mitmproxy.css" rel="stylesheet">
|
||||
<link href="/static/syntax.css" rel="stylesheet">
|
||||
<script src="/static/jquery-1.7.2.min.js"></script>
|
||||
<script src="/static/jquery.scrollTo-min.js"></script>
|
||||
<script src="/static/jquery.localscroll-min.js"></script>
|
||||
<script src="/static/bootstrap.min.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-top: 60px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="navbar navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</a>
|
||||
<a class="brand" href="/">mitmproxy</a>
|
||||
<div class="nav-collapse">
|
||||
<ul class="nav">
|
||||
<li {% if section== "home" %} class="active" {% endif %}><a href="/">home</a></li>
|
||||
<li {% if section== "certs" %} class="active" {% endif %}><a href="/certs">certs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar navbar-default" role="navigation">
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<hr>
|
||||
<footer>
|
||||
<span><a href="http://mitmproxy.org">mitmproxy</a></span>
|
||||
</footer>
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="#">mitmproxy</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<script>
|
||||
$(function(){
|
||||
$.localScroll(
|
||||
{
|
||||
duration: 300,
|
||||
offset: {top: -45}
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Flask>=0.9
|
||||
Jinja2>=2.7
|
||||
MarkupSafe>=0.18
|
||||
PIL>=1.1.7
|
||||
Pillow>=2.3.0,<2.4
|
||||
Werkzeug>=0.8.3
|
||||
lxml>=3.2.1
|
||||
netlib>=0.9.2
|
||||
|
@ -13,4 +13,5 @@ pyasn1>=0.1.7
|
|||
requests>=1.2.2
|
||||
urwid>=1.1.1
|
||||
wsgiref>=0.1.2
|
||||
jsbeautifier>=1.4.0
|
||||
jsbeautifier>=1.4.0
|
||||
cssutils>=1.0,<1.1
|
||||
|
|
2
setup.py
2
setup.py
|
@ -97,7 +97,7 @@ setup(
|
|||
"urwid>=1.1",
|
||||
"pyasn1>0.1.2",
|
||||
"pyopenssl>=0.13",
|
||||
"PIL",
|
||||
"Pillow>=2.3.0,<2.4",
|
||||
"lxml",
|
||||
"flask"
|
||||
],
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
body,html{height:100%}body{font-family:'Open Sans',sans-serif;font-size:1.5em;padding-top:80px}
|
|
@ -13,6 +13,11 @@ try:
|
|||
except ImportError:
|
||||
pyamf = None
|
||||
|
||||
try:
|
||||
import cssutils
|
||||
except:
|
||||
cssutils = None
|
||||
|
||||
|
||||
class TestContentView:
|
||||
def test_trailer(self):
|
||||
|
@ -112,6 +117,26 @@ class TestContentView:
|
|||
assert v([], "[1, 2, 3", 100)
|
||||
assert v([], "function(a){[1, 2, 3]}", 100)
|
||||
|
||||
def test_view_css(self):
|
||||
v = cv.ViewCSS()
|
||||
|
||||
with open('./test/data/1.css', 'r') as fp:
|
||||
fixture_1 = fp.read()
|
||||
|
||||
result = v([], 'a', 100)
|
||||
|
||||
if cssutils:
|
||||
assert len(result[1]) == 0
|
||||
else:
|
||||
assert len(result[1]) == 1
|
||||
|
||||
result = v([], fixture_1, 100)
|
||||
|
||||
if cssutils:
|
||||
assert len(result[1]) > 1
|
||||
else:
|
||||
assert len(result[1]) == 1
|
||||
|
||||
def test_view_hex(self):
|
||||
v = cv.ViewHex()
|
||||
assert v([], "foo", 1000)
|
||||
|
@ -250,3 +275,101 @@ if cv.ViewProtobuf.is_available():
|
|||
|
||||
def test_get_by_shortcut():
|
||||
assert cv.get_by_shortcut("h")
|
||||
|
||||
def test_search_highlights():
|
||||
# Default text in requests is content. We will search for nt once, and
|
||||
# expect the first bit to be highlighted. We will do it again and expect the
|
||||
# second to be.
|
||||
f = tutils.tflowview()
|
||||
|
||||
f.search("nt")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 0)
|
||||
assert text_object.get_text() == ('content', [(None, 2), (f.highlight_color, 2)])
|
||||
|
||||
f.search("nt")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 1)
|
||||
assert text_object.get_text() == ('content', [(None, 5), (f.highlight_color, 2)])
|
||||
|
||||
def test_search_returns_useful_messages():
|
||||
f = tutils.tflowview()
|
||||
|
||||
# original string is content. this string should not be in there.
|
||||
response = f.search("oranges and other fruit.")
|
||||
assert response == "no matches for 'oranges and other fruit.'"
|
||||
|
||||
def test_search_highlights_clears_prev():
|
||||
f = tutils.tflowview(request_contents="this is string\nstring is string")
|
||||
|
||||
f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 0)
|
||||
assert text_object.get_text() == ('this is string', [(None, 8), (f.highlight_color, 6)])
|
||||
|
||||
# search again, it should not be highlighted again.
|
||||
f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 0)
|
||||
assert text_object.get_text() != ('this is string', [(None, 8), (f.highlight_color, 6)])
|
||||
|
||||
def test_search_highlights_multi_line():
|
||||
f = tutils.tflowview(request_contents="this is string\nstring is string")
|
||||
|
||||
# should highlight the first line.
|
||||
f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 0)
|
||||
assert text_object.get_text() == ('this is string', [(None, 8), (f.highlight_color, 6)])
|
||||
|
||||
# should highlight second line, first appearance of string.
|
||||
f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 1)
|
||||
assert text_object.get_text() == ('string is string', [(None, 0), (f.highlight_color, 6)])
|
||||
|
||||
# should highlight third line, second appearance of string.
|
||||
f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 1)
|
||||
assert text_object.get_text() == ('string is string', [(None, 10), (f.highlight_color, 6)])
|
||||
|
||||
def test_search_loops():
|
||||
f = tutils.tflowview(request_contents="this is string\nstring is string")
|
||||
|
||||
# get to the end.
|
||||
f.search("string")
|
||||
f.search("string")
|
||||
f.search("string")
|
||||
|
||||
# should highlight the first line.
|
||||
message = f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 0)
|
||||
assert text_object.get_text() == ('this is string', [(None, 8), (f.highlight_color, 6)])
|
||||
assert message == "search hit BOTTOM, continuing at TOP"
|
||||
|
||||
def test_search_focuses():
|
||||
f = tutils.tflowview(request_contents="this is string\nstring is string")
|
||||
|
||||
# should highlight the first line.
|
||||
f.search("string")
|
||||
|
||||
# should be focusing on the 2nd text line.
|
||||
f.search("string")
|
||||
text_object = tutils.get_body_line(f.last_displayed_body, 1)
|
||||
assert f.last_displayed_body.focus == text_object
|
||||
|
||||
def test_search_does_not_crash_on_bad():
|
||||
"""
|
||||
this used to crash, kept for reference.
|
||||
"""
|
||||
|
||||
f = tutils.tflowview(request_contents="this is string\nstring is string\n"+("A" * cv.VIEW_CUTOFF)+"AFTERCUTOFF")
|
||||
f.search("AFTERCUTOFF")
|
||||
|
||||
# pretend F
|
||||
f.state.add_flow_setting(
|
||||
f.flow,
|
||||
(f.state.view_flow_mode, "fullcontents"),
|
||||
True
|
||||
)
|
||||
f.master.refresh_flow(f.flow)
|
||||
|
||||
# text changed, now this string will exist. can happen when user presses F
|
||||
# for full text view
|
||||
f.search("AFTERCUTOFF")
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class TestMaster:
|
|||
def test_default_handler(self):
|
||||
m = controller.Master(None)
|
||||
msg = mock.MagicMock()
|
||||
m.handle(msg)
|
||||
m.handle("type", msg)
|
||||
assert msg.reply.call_count == 1
|
||||
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ class TestDumpMaster:
|
|||
o = dump.Options(app=True)
|
||||
s = mock.MagicMock()
|
||||
m = dump.DumpMaster(s, o, None)
|
||||
assert s.apps.add.call_count == 1
|
||||
assert len(m.apps.apps) == 1
|
||||
|
||||
def test_replacements(self):
|
||||
o = dump.Options(replacements=[(".*", "content", "foo")])
|
||||
|
|
|
@ -5,6 +5,27 @@ from libmproxy import filt, flow, controller, utils, tnetstring, proxy
|
|||
import tutils
|
||||
|
||||
|
||||
def test_app_registry():
|
||||
ar = flow.AppRegistry()
|
||||
ar.add("foo", "domain", 80)
|
||||
|
||||
r = tutils.treq()
|
||||
r.host = "domain"
|
||||
r.port = 80
|
||||
assert ar.get(r)
|
||||
|
||||
r.port = 81
|
||||
assert not ar.get(r)
|
||||
|
||||
r = tutils.treq()
|
||||
r.host = "domain2"
|
||||
r.port = 80
|
||||
assert not ar.get(r)
|
||||
r.headers["host"] = ["domain"]
|
||||
assert ar.get(r)
|
||||
|
||||
|
||||
|
||||
class TestStickyCookieState:
|
||||
def _response(self, cookie, host):
|
||||
s = flow.StickyCookieState(filt.parse(".*"))
|
||||
|
|
|
@ -11,26 +11,6 @@ def test_proxy_error():
|
|||
assert str(p)
|
||||
|
||||
|
||||
def test_app_registry():
|
||||
ar = proxy.AppRegistry()
|
||||
ar.add("foo", "domain", 80)
|
||||
|
||||
r = tutils.treq()
|
||||
r.host = "domain"
|
||||
r.port = 80
|
||||
assert ar.get(r)
|
||||
|
||||
r.port = 81
|
||||
assert not ar.get(r)
|
||||
|
||||
r = tutils.treq()
|
||||
r.host = "domain2"
|
||||
r.port = 80
|
||||
assert not ar.get(r)
|
||||
r.headers["host"] = ["domain"]
|
||||
assert ar.get(r)
|
||||
|
||||
|
||||
class TestServerConnection:
|
||||
def setUp(self):
|
||||
self.d = test.Daemon()
|
||||
|
|
|
@ -176,10 +176,10 @@ class TestHTTPAuth(tservers.HTTPProxTest):
|
|||
class TestHTTPConnectSSLError(tservers.HTTPProxTest):
|
||||
certfile = True
|
||||
def test_go(self):
|
||||
p = self.pathoc()
|
||||
req = "connect:'localhost:%s'"%self.proxy.port
|
||||
assert p.request(req).status_code == 200
|
||||
assert p.request(req).status_code == 400
|
||||
p = self.pathoc_raw()
|
||||
dst = ("localhost", self.proxy.port)
|
||||
p.connect(connect_to=dst)
|
||||
tutils.raises("400 - Bad Request", p.http_connect, dst)
|
||||
|
||||
|
||||
class TestHTTPS(tservers.HTTPProxTest, CommonMixin):
|
||||
|
|
|
@ -23,10 +23,10 @@ def errapp(environ, start_response):
|
|||
class TestMaster(flow.FlowMaster):
|
||||
def __init__(self, testq, config):
|
||||
s = proxy.ProxyServer(config, 0)
|
||||
s.apps.add(testapp, "testapp", 80)
|
||||
s.apps.add(errapp, "errapp", 80)
|
||||
state = flow.State()
|
||||
flow.FlowMaster.__init__(self, s, state)
|
||||
self.apps.add(testapp, "testapp", 80)
|
||||
self.apps.add(errapp, "errapp", 80)
|
||||
self.testq = testq
|
||||
self.clear_log()
|
||||
self.start_app(APP_HOST, APP_PORT, False)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import os, shutil, tempfile
|
||||
from contextlib import contextmanager
|
||||
from libmproxy import flow, utils, controller
|
||||
from libmproxy.console.flowview import FlowView
|
||||
from libmproxy.console import ConsoleState
|
||||
from netlib import certutils
|
||||
from nose.plugins.skip import SkipTest
|
||||
from mock import Mock
|
||||
|
||||
def _SkipWindows():
|
||||
raise SkipTest("Skipped on Windows.")
|
||||
|
@ -12,13 +15,14 @@ def SkipWindows(fn):
|
|||
else:
|
||||
return fn
|
||||
|
||||
def treq(conn=None):
|
||||
def treq(conn=None, content="content"):
|
||||
if not conn:
|
||||
conn = flow.ClientConnect(("address", 22))
|
||||
conn.reply = controller.DummyReply()
|
||||
headers = flow.ODictCaseless()
|
||||
headers["header"] = ["qvalue"]
|
||||
r = flow.Request(conn, (1, 1), "host", 80, "http", "GET", "/path", headers, "content")
|
||||
r = flow.Request(conn, (1, 1), "host", 80, "http", "GET", "/path", headers,
|
||||
content)
|
||||
r.reply = controller.DummyReply()
|
||||
return r
|
||||
|
||||
|
@ -41,8 +45,9 @@ def terr(req=None):
|
|||
return err
|
||||
|
||||
|
||||
def tflow():
|
||||
r = treq()
|
||||
def tflow(r=None):
|
||||
if r == None:
|
||||
r = treq()
|
||||
return flow.Flow(r)
|
||||
|
||||
|
||||
|
@ -57,6 +62,20 @@ def tflow_err():
|
|||
f.error = terr(f.request)
|
||||
return f
|
||||
|
||||
def tflowview(request_contents=None):
|
||||
m = Mock()
|
||||
cs = ConsoleState()
|
||||
if request_contents == None:
|
||||
flow = tflow()
|
||||
else:
|
||||
req = treq(None, request_contents)
|
||||
flow = tflow(req)
|
||||
|
||||
fv = FlowView(m, cs, flow)
|
||||
return fv
|
||||
|
||||
def get_body_line(last_displayed_body, line_nb):
|
||||
return last_displayed_body.contents()[line_nb + 2]
|
||||
|
||||
@contextmanager
|
||||
def tmpdir(*args, **kwargs):
|
||||
|
|
Loading…
Reference in New Issue