From 2b4af8d4756bc4eef613ec2cdf81a97d3952150b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 13 Jun 2013 16:04:04 +0200 Subject: add support for multiple scripts and script arguments. refs #76 --- libmproxy/cmdline.py | 9 +++++--- libmproxy/console/__init__.py | 2 +- libmproxy/dump.py | 10 ++++---- libmproxy/flow.py | 53 +++++++++++++++++++++---------------------- libmproxy/script.py | 20 +++++++++------- 5 files changed, 50 insertions(+), 44 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index b76792cf..75f28bca 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -16,6 +16,8 @@ import proxy import re, filt import argparse +import shlex +import os class ParseException(Exception): pass class OptionException(Exception): pass @@ -151,7 +153,7 @@ def get_common_options(options): replacements = reps, setheaders = setheaders, server_replay = options.server_replay, - script = options.script, + scripts = options.scripts, stickycookie = stickycookie, stickyauth = stickyauth, showhost = options.showhost, @@ -209,8 +211,9 @@ def common_options(parser): ) parser.add_argument( "-s", - action="store", dest="script", default=None, - help="Run a script." + action="append", type=lambda x: shlex.split(x,posix=(os.name != "nt")), dest="scripts", default=[], + metavar='"script.py --bar"', + help="Run a script. Surround with quotes to pass script arguments. Can be passed multiple times." ) parser.add_argument( "-t", diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index ba699d33..bb57ba26 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -460,7 +460,7 @@ class ConsoleMaster(flow.FlowMaster): self._run_script_method("response", s, f) if f.error: self._run_script_method("error", s, f) - s.run("done") + s.unload() self.refresh_flow(f) self.state.last_script = path diff --git a/libmproxy/dump.py b/libmproxy/dump.py index b1022ef5..a4846da7 100644 --- a/libmproxy/dump.py +++ b/libmproxy/dump.py @@ -36,7 +36,7 @@ class Options(object): "rheaders", "setheaders", "server_replay", - "script", + "scripts", "showhost", "stickycookie", "stickyauth", @@ -121,8 +121,8 @@ class DumpMaster(flow.FlowMaster): not options.keepserving ) - if options.script: - err = self.load_script(options.script) + for script_argv in options.scripts: + err = self.load_script(script_argv) if err: raise DumpError(err) @@ -230,8 +230,8 @@ class DumpMaster(flow.FlowMaster): def run(self): # pragma: no cover if self.o.rfile and not self.o.keepserving: - if self.script: - self.load_script(None) + for script in self.scripts: + self.unload_script(script) return try: return flow.FlowMaster.run(self) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 10c5da5d..61b2a241 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -1357,7 +1357,7 @@ class FlowMaster(controller.Master): self.server_playback = None self.client_playback = None self.kill_nonreplay = False - self.script = None + self.scripts = [] self.pause_scripts = False self.stickycookie_state = False @@ -1381,37 +1381,43 @@ class FlowMaster(controller.Master): """ pass - def get_script(self, path): + def get_script(self, script_argv): """ Returns an (error, script) tuple. """ - s = script.Script(path, ScriptContext(self)) + s = script.Script(script_argv, ScriptContext(self)) try: s.load() except script.ScriptError, v: return (v.args[0], None) - ret = s.run("start") - if not ret[0] and ret[1]: - return ("Error in script start:\n\n" + ret[1][1], None) return (None, s) - def load_script(self, path): + def unload_script(self,script): + script.unload() + self.scripts.remove(script) + + def load_script(self, script_argv): """ Loads a script. Returns an error description if something went - wrong. If path is None, the current script is terminated. + wrong. """ - if path is None: - self.run_script_hook("done") - self.script = None + r = self.get_script(script_argv) + if r[0]: + return r[0] else: - r = self.get_script(path) - if r[0]: - return r[0] - else: - if self.script: - self.run_script_hook("done") - self.script = r[1] + self.scripts.append(r[1]) + + def run_single_script_hook(self, script, name, *args, **kwargs): + if script and not self.pause_scripts: + ret = script.run(name, *args, **kwargs) + if not ret[0] and ret[1]: + e = "Script error:\n" + ret[1][1] + self.add_event(e, "error") + def 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) @@ -1561,13 +1567,6 @@ class FlowMaster(controller.Master): if block: rt.join() - def run_script_hook(self, name, *args, **kwargs): - if self.script and not self.pause_scripts: - ret = self.script.run(name, *args, **kwargs) - if not ret[0] and ret[1]: - e = "Script error:\n" + ret[1][1] - self.add_event(e, "error") - def handle_clientconnect(self, cc): self.run_script_hook("clientconnect", cc) cc.reply() @@ -1609,8 +1608,8 @@ class FlowMaster(controller.Master): return f def shutdown(self): - if self.script: - self.load_script(None) + for script in self.scripts: + self.unload_script(script) controller.Master.shutdown(self) if self.stream: for i in self.state._flow_list: diff --git a/libmproxy/script.py b/libmproxy/script.py index 4ffac71b..4676672b 100644 --- a/libmproxy/script.py +++ b/libmproxy/script.py @@ -23,12 +23,12 @@ class Script: """ The instantiator should do something along this vein: - s = Script(path, master) + s = Script(argv, master) s.load() - s.run("start") """ - def __init__(self, path, ctx): - self.path, self.ctx = path, ctx + def __init__(self, argv, ctx): + self.argv = argv + self.ctx = ctx self.ns = None def load(self): @@ -38,17 +38,21 @@ class Script: Raises ScriptError on failure, with argument equal to an error message that may be a formatted traceback. """ - path = os.path.expanduser(self.path) + path = os.path.expanduser(self.argv[0]) if not os.path.exists(path): - raise ScriptError("No such file: %s"%self.path) + raise ScriptError("No such file: %s" % path) if not os.path.isfile(path): - raise ScriptError("Not a file: %s"%self.path) + raise ScriptError("Not a file: %s" % path) ns = {} try: execfile(path, ns, ns) + self.ns = ns + self.run("start", self.argv) except Exception, v: raise ScriptError(traceback.format_exc(v)) - self.ns = ns + + def unload(self): + return self.run("done") def run(self, name, *args, **kwargs): """ -- cgit v1.2.3 From 65d1ed1b3c54985b4b4b0bec919de0ad7a86e342 Mon Sep 17 00:00:00 2001 From: JC Date: Fri, 30 Aug 2013 17:14:18 -0700 Subject: Added -F http[s]://server:port option that allows MITM to forward traffic to another http server upstream. --- libmproxy/cmdline.py | 5 +++++ libmproxy/proxy.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index a394d7f3..5e120ce1 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -189,6 +189,11 @@ def common_options(parser): action="store", dest="reverse_proxy", default=None, help="Reverse proxy to upstream server: http[s]://host[:port]" ) + parser.add_argument( + "-F", + action="store", dest="forward_proxy", default=None, + help="Proxy to unconditionally forward to: http[s]://host[:port]" + ) parser.add_argument( "-q", action="store_true", dest="quiet", diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 75a54192..81838e44 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -23,13 +23,14 @@ class Log: class ProxyConfig: - def __init__(self, certfile = None, cacert = None, clientcerts = None, no_upstream_cert=False, body_size_limit = None, reverse_proxy=None, transparent_proxy=None, authenticator=None): + 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 self.cacert = cacert self.clientcerts = clientcerts self.no_upstream_cert = no_upstream_cert self.body_size_limit = body_size_limit self.reverse_proxy = reverse_proxy + self.forward_proxy = forward_proxy self.transparent_proxy = transparent_proxy self.authenticator = authenticator self.certstore = certutils.CertStore() @@ -219,7 +220,12 @@ class ProxyHandler(tcp.BaseHandler): # the case, we want to reconnect without sending an error # to the client. while 1: - sc = self.get_server_connection(cc, scheme, host, port, self.sni) + if self.config.forward_proxy: + forward_scheme, forward_host, forward_port = self.config.forward_proxy + sc = self.get_server_connection(cc, forward_scheme, forward_host, forward_port, self.sni) + else: + sc = self.get_server_connection(cc, scheme, host, port, self.sni) + sc.send(request) if sc.requestcount == 1: # add timestamps only for first request (others are not directly affected) request.tcp_setup_timestamp = sc.tcp_setup_timestamp @@ -594,6 +600,13 @@ def process_proxy_options(parser, options): else: rp = None + if options.forward_proxy: + fp = utils.parse_proxy_spec(options.forward_proxy) + if not fp: + return parser.error("Invalid forward proxy specification: %s"%options.forward_proxy) + else: + fp = None + if options.clientcerts: options.clientcerts = os.path.expanduser(options.clientcerts) if not os.path.exists(options.clientcerts) or not os.path.isdir(options.clientcerts): @@ -623,6 +636,7 @@ def process_proxy_options(parser, options): body_size_limit = body_size_limit, no_upstream_cert = options.no_upstream_cert, reverse_proxy = rp, + forward_proxy = fp, transparent_proxy = trans, authenticator = authenticator ) -- cgit v1.2.3 From f33d128a7f27eb2103e511b830c00fe09091c448 Mon Sep 17 00:00:00 2001 From: Jaime Soriano Pastor Date: Thu, 26 Sep 2013 12:23:48 +0200 Subject: Reverse proxy works with SSL --- libmproxy/proxy.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 75a54192..826726c8 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -414,10 +414,21 @@ class ProxyHandler(tcp.BaseHandler): ) def read_request_reverse(self, client_conn): + scheme, host, port = self.config.reverse_proxy + if scheme.lower() == "https": + if not self.ssl_established: + dummycert = self.find_cert(client_conn, host, port, host) + sni = HandleSNI( + self, client_conn, host, port, + dummycert, self.config.certfile or self.config.cacert + ) + try: + self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) + except tcp.NetLibError, v: + raise ProxyError(400, str(v)) line = self.get_line(self.rfile) if line == "": return None - scheme, host, port = self.config.reverse_proxy r = http.parse_init_http(line) if not r: raise ProxyError(400, "Bad HTTP request line: %s"%repr(line)) @@ -427,7 +438,7 @@ class ProxyHandler(tcp.BaseHandler): self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit ) return flow.Request( - client_conn, httpversion, host, port, "http", method, path, headers, content, + client_conn, httpversion, host, port, scheme, method, path, headers, content, self.rfile.first_byte_timestamp, utils.timestamp() ) -- cgit v1.2.3 From 7140323bdbd5fe03d23efff7ca71265a29f3e058 Mon Sep 17 00:00:00 2001 From: Jaime Soriano Pastor Date: Thu, 26 Sep 2013 12:38:13 +0200 Subject: New method establish_ssl to avoid duplicated code --- libmproxy/proxy.py | 44 +++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 826726c8..394db493 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -310,6 +310,17 @@ class ProxyHandler(tcp.BaseHandler): raise ProxyError(502, "Unable to generate dummy cert.") return ret + def establish_ssl(self, client_conn, host, port): + dummycert = self.find_cert(client_conn, host, port, host) + sni = HandleSNI( + self, client_conn, host, port, + dummycert, self.config.certfile or self.config.cacert + ) + try: + self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) + except tcp.NetLibError, v: + raise ProxyError(400, str(v)) + def get_line(self, fp): """ Get a line, possibly preceded by a blank. @@ -329,15 +340,7 @@ class ProxyHandler(tcp.BaseHandler): if port in self.config.transparent_proxy["sslports"]: scheme = "https" if not self.ssl_established: - dummycert = self.find_cert(client_conn, host, port, host) - sni = HandleSNI( - self, client_conn, host, port, - dummycert, self.config.certfile or self.config.cacert - ) - try: - self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) - except tcp.NetLibError, v: - raise ProxyError(400, str(v)) + self.establish_ssl(client_conn, host, port) else: scheme = "http" line = self.get_line(self.rfile) @@ -372,15 +375,7 @@ class ProxyHandler(tcp.BaseHandler): '\r\n' ) self.wfile.flush() - dummycert = self.find_cert(client_conn, host, port, host) - sni = HandleSNI( - self, client_conn, host, port, - dummycert, self.config.certfile or self.config.cacert - ) - try: - self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) - except tcp.NetLibError, v: - raise ProxyError(400, str(v)) + self.establish_ssl(client_conn, host, port) self.proxy_connect_state = (host, port, httpversion) line = self.rfile.readline(line) @@ -415,17 +410,8 @@ class ProxyHandler(tcp.BaseHandler): def read_request_reverse(self, client_conn): scheme, host, port = self.config.reverse_proxy - if scheme.lower() == "https": - if not self.ssl_established: - dummycert = self.find_cert(client_conn, host, port, host) - sni = HandleSNI( - self, client_conn, host, port, - dummycert, self.config.certfile or self.config.cacert - ) - try: - self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) - except tcp.NetLibError, v: - raise ProxyError(400, str(v)) + if scheme.lower() == "https" and not self.ssl_established: + self.establish_ssl(client_conn, host, port) line = self.get_line(self.rfile) if line == "": return None -- cgit v1.2.3 From 675518f8735c3f70e25bc448c804ac0fd506a43c Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 18 Nov 2013 17:25:52 +0100 Subject: add serverconnect script hook --- libmproxy/flow.py | 7 +++++++ libmproxy/proxy.py | 1 + 2 files changed, 8 insertions(+) (limited to 'libmproxy') diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 24042812..40b7e535 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -1580,6 +1580,13 @@ class FlowMaster(controller.Master): self.run_script_hook("clientdisconnect", r) 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. + self.run_script_hook("serverconnect", sc) + sc.reply() + def handle_error(self, r): f = self.state.add_error(r) if f: diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 75a54192..94f358bc 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -158,6 +158,7 @@ class ProxyHandler(tcp.BaseHandler): if not self.server_conn: try: self.server_conn = ServerConnection(self.config, scheme, host, port, sni) + self.channel.ask(self.server_conn) self.server_conn.connect() except tcp.NetLibError, v: raise ProxyError(502, v) -- cgit v1.2.3 From d4c3b1c21355a46219c58e23d7542fa059af7573 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 19 Nov 2013 04:08:16 +0100 Subject: attempt to fix https://github.com/mitmproxy/netlib/issues/24 --- libmproxy/proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'libmproxy') diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 75a54192..790ae18a 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -258,13 +258,13 @@ class ProxyHandler(tcp.BaseHandler): else: response = response_reply self.send_response(response) - if request and http.request_connection_close(request.httpversion, request.headers): + if request and http.connection_close(request.httpversion, request.headers): return # We could keep the client connection when the server # connection needs to go away. However, we want to mimic # behaviour as closely as possible to the client, so we # disconnect. - if http.response_connection_close(response.httpversion, response.headers): + if http.connection_close(response.httpversion, response.headers): return except (IOError, ProxyError, http.HttpError, tcp.NetLibError), e: if hasattr(e, "code"): -- cgit v1.2.3 From 9a986e0c1b0cebc494a709d9c12eca7f69b9fd41 Mon Sep 17 00:00:00 2001 From: Rich Somerfield Date: Tue, 26 Nov 2013 14:14:51 -0800 Subject: Fix divide by zero issue when timestamp start and end are the same --- libmproxy/console/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libmproxy') diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 006303a7..951d2c2a 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -190,7 +190,7 @@ def format_flow(f, focus, extended=False, hostheader=False, padding=2): delta = f.response.timestamp_end - f.response.timestamp_start size = len(f.response.content) + f.response.get_header_size() - rate = utils.pretty_size(size / delta) + rate = utils.pretty_size(size / ( delta if delta > 0 else 1 ) ) d.update(dict( resp_code = f.response.code, -- cgit v1.2.3