continue work on the proxyhandler

This commit is contained in:
Maximilian Hils 2014-01-07 02:29:10 +01:00
commit ea2f17680b
29 changed files with 669 additions and 295 deletions

View File

@ -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)

View File

@ -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():

View File

@ -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",

View File

@ -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(

View File

@ -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(),

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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")]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -97,7 +97,7 @@ setup(
"urwid>=1.1",
"pyasn1>0.1.2",
"pyopenssl>=0.13",
"PIL",
"Pillow>=2.3.0,<2.4",
"lxml",
"flask"
],

1
test/data/1.css Normal file
View File

@ -0,0 +1 @@
body,html{height:100%}body{font-family:'Open Sans',sans-serif;font-size:1.5em;padding-top:80px}

View File

@ -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")

View File

@ -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

View File

@ -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")])

View File

@ -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(".*"))

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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):