From ece15b3c8af790e425ebcaa6807d6e133d810ff6 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 8 Sep 2014 14:43:05 +0200 Subject: [PATCH 01/89] reverse proxy: adjust dst when reading flows, fix #346 --- libmproxy/flow.py | 5 +++++ test/test_flow.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index d263ccdde..6aa26f4a9 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -615,6 +615,11 @@ class FlowMaster(controller.Master): """ Loads a flow, and returns a new flow object. """ + + if self.server and self.server.config.mode == "reverse": + f.request.host, f.request.port = self.server.config.mode.dst[2:] + f.request.scheme = "https" if self.server.config.mode.dst[1] else "http" + f.reply = controller.DummyReply() if f.request: self.handle_request(f) diff --git a/test/test_flow.py b/test/test_flow.py index a297bf5f4..399c88276 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -5,8 +5,9 @@ import mock from libmproxy import filt, protocol, controller, utils, tnetstring, flow from libmproxy.protocol.primitives import Error, Flow from libmproxy.protocol.http import decoded, CONTENT_MISSING -from libmproxy.proxy.connection import ClientConnection, ServerConnection -from netlib import tcp +from libmproxy.proxy import ProxyConfig +from libmproxy.proxy.server import DummyServer +from libmproxy.proxy.connection import ClientConnection import tutils @@ -491,6 +492,14 @@ class TestSerialize: fm.load_flows(r) assert len(s._flow_list) == 6 + def test_load_flows_reverse(self): + r = self._treader() + s = flow.State() + conf = ProxyConfig(mode="reverse", upstream_server=[True,True,"use-this-domain",80]) + fm = flow.FlowMaster(DummyServer(conf), s) + fm.load_flows(r) + assert s._flow_list[0].request.host == "use-this-domain" + def test_filter(self): sio = StringIO() fl = filt.parse("~c 200") From 6ce6b1ad69df886af6da025c16d4d6916a56da2c Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 2 Oct 2014 00:58:40 +0200 Subject: [PATCH 02/89] replay: carry over SNI value --- MANIFEST.in | 5 +---- libmproxy/protocol/http.py | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index efe18f43c..cc048b614 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,10 +2,7 @@ include mitmproxy mitmdump include LICENSE CHANGELOG CONTRIBUTORS README.txt exclude README.mkd recursive-include examples * -recursive-exclude examples *.pyc *.pyo *.swo *.swp recursive-include doc * -recursive-exclude doc *.pyc *.pyo *.swo *.swp recursive-include test * -recursive-exclude test *.pyc *.pyo *.swo *.swp recursive-include libmproxy * -recursive-exclude libmproxy *.pyc *.pyo *.swo *.swp \ No newline at end of file +recursive-exclude * *.pyc *.pyo *.swo *.swp \ No newline at end of file diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index de5f99501..0bb014a2c 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1304,7 +1304,7 @@ class RequestReplayThread(threading.Thread): server.connect() if r.scheme == "https": send_connect_request(server, r.host, r.port) - server.establish_ssl(self.config.clientcerts, sni=r.host) + server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) r.form_out = "relative" else: r.form_out = "absolute" @@ -1313,10 +1313,11 @@ class RequestReplayThread(threading.Thread): server = ServerConnection(server_address) server.connect() if r.scheme == "https": - server.establish_ssl(self.config.clientcerts, sni=r.host) + server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) r.form_out = "relative" server.send(r.assemble()) + self.flow.server_conn = server self.flow.response = HTTPResponse.from_stream(server.rfile, r.method, body_size_limit=self.config.body_size_limit) self.channel.ask("response", self.flow) From d5c318b070305ac51e6b37f80336ab471af28d26 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 8 Oct 2014 20:44:52 +0200 Subject: [PATCH 03/89] fix support for chained certificates --- examples/flowbasic | 2 +- examples/stickycookies | 2 +- libmproxy/proxy/config.py | 4 ++-- libmproxy/proxy/server.py | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/flowbasic b/examples/flowbasic index 21d31efa3..b99266c88 100755 --- a/examples/flowbasic +++ b/examples/flowbasic @@ -36,7 +36,7 @@ class MyMaster(flow.FlowMaster): config = proxy.ProxyConfig( port=8080, - ca_file=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem") + default_ca=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem") ) state = flow.State() server = ProxyServer(config) diff --git a/examples/stickycookies b/examples/stickycookies index 132e4dc75..94adfcf80 100755 --- a/examples/stickycookies +++ b/examples/stickycookies @@ -38,7 +38,7 @@ class StickyMaster(controller.Master): config = proxy.ProxyConfig( port=8080, - ca_file=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem") + default_ca=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem") ) server = ProxyServer(config) m = StickyMaster(server) diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index 62104a249..24e09b6a1 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -16,7 +16,7 @@ def parse_host_pattern(patterns): class ProxyConfig: def __init__(self, host='', port=8080, server_version=version.NAMEVERSION, - confdir=CONF_DIR, ca_file=None, clientcerts=None, + confdir=CONF_DIR, default_ca=None, clientcerts=None, no_upstream_cert=False, body_size_limit=None, mode=None, upstream_server=None, http_form_in=None, http_form_out=None, authenticator=None, ignore=[], @@ -45,7 +45,7 @@ class ProxyConfig: self.ignore = parse_host_pattern(ignore) self.authenticator = authenticator self.confdir = os.path.expanduser(confdir) - self.ca_file = ca_file or os.path.join(self.confdir, CONF_BASENAME + "-ca.pem") + self.default_ca = default_ca or os.path.join(self.confdir, CONF_BASENAME + "-ca.pem") self.certstore = certutils.CertStore.from_store(self.confdir, CONF_BASENAME) for spec, cert in certs: self.certstore.add_cert_file(spec, cert) diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 307a4bcdb..0152f5399 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -190,14 +190,14 @@ class ConnectionHandler: if client: if self.client_conn.ssl_established: raise ProxyError(502, "SSL to Client already established.") - cert, key = self.find_cert() + cert, key, chain_file = self.find_cert() try: self.client_conn.convert_to_ssl( cert, key, handle_sni=self.handle_sni, cipher_list=self.config.ciphers, dhparams=self.config.certstore.dhparams, - ca_file=self.config.ca_file + chain_file=chain_file ) except tcp.NetLibError as v: raise ProxyError(400, repr(v)) @@ -264,17 +264,17 @@ class ConnectionHandler: self.log("SNI received: %s" % self.sni, "debug") self.server_reconnect() # reconnect to upstream server with SNI # Now, change client context to reflect changed certificate: - cert, key = self.find_cert() + cert, key, chain_file = self.find_cert() new_context = self.client_conn._create_ssl_context( cert, key, method=SSL.TLSv1_METHOD, cipher_list=self.config.ciphers, dhparams=self.config.certstore.dhparams, - ca_file=self.config.ca_file + chain_file=chain_file ) 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: # pragma: no cover + except: # pragma: no cover import traceback self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error") \ No newline at end of file From f04693c04779b6c78d0370c0ffd15f899b9b522f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 8 Oct 2014 21:41:03 +0200 Subject: [PATCH 04/89] fix typo --- libmproxy/protocol/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 0bb014a2c..32a88b4b9 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1079,7 +1079,7 @@ class HTTPHandler(ProtocolHandler): if message: self.c.log(message, level="info") if message_debug: - self.c.log(message, level="debug") + self.c.log(message_debug, level="debug") if flow: # TODO: no flows without request or with both request and response at the moment. From 5b33f7896136012ab8cd86999f5af2b90e66125b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 9 Oct 2014 00:49:11 +0200 Subject: [PATCH 05/89] add mini documentation --- libmproxy/proxy/config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index 24e09b6a1..b59748072 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -133,10 +133,12 @@ def ssl_option_group(parser): group.add_argument( "--cert", dest='certs', default=[], type=str, metavar="SPEC", action="append", - help='Add an SSL certificate. SPEC is of the form "[domain=]path". ' \ - 'The domain may include a wildcard, and is equal to "*" if not specified. ' \ - 'The file at path is a certificate in PEM format. If a private key is included in the PEM, ' \ - 'it is used, else the default key in the conf dir is used. Can be passed multiple times.' + help='Add an SSL certificate. SPEC is of the form "[domain=]path". ' + 'The domain may include a wildcard, and is equal to "*" if not specified. ' + 'The file at path is a certificate in PEM format. If a private key is included in the PEM, ' + 'it is used, else the default key in the conf dir is used. ' + 'The PEM file should contain the full certificate chain, with the leaf certificate as the first entry. ' + 'Can be passed multiple times.' ) group.add_argument( "--client-certs", action="store", From 7c56a3bb019f521fc45953923b94e9249a1fca78 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 9 Oct 2014 01:58:54 +0200 Subject: [PATCH 06/89] Add SOCKS5 mode, fix #305 --- libmproxy/cmdline.py | 8 +++- libmproxy/proxy/config.py | 9 +++- libmproxy/proxy/primitives.py | 81 ++++++++++++++++++++++++++++++++--- libmproxy/proxy/server.py | 2 +- 4 files changed, 89 insertions(+), 11 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index f6cd1ab84..fe68e95ee 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -1,6 +1,5 @@ from __future__ import absolute_import import re -import argparse from argparse import ArgumentTypeError from netlib import http from . import filt, utils @@ -287,6 +286,11 @@ def common_options(parser): action="store", type=parse_server_spec, dest="reverse_proxy", default=None, help="Forward all requests to upstream HTTP server: http[s][2http[s]]://host[:port]" ) + group.add_argument( + "--socks", + action="store_true", dest="socks_proxy", default=False, + help="Set SOCKS5 proxy mode." + ) group.add_argument( "-T", action="store_true", dest="transparent_proxy", default=False, @@ -381,7 +385,7 @@ def common_options(parser): action="append", dest="replay_ignore_params", type=str, help="Request's parameters to be ignored while searching for a saved flow to replay" "Can be passed multiple times." - ) + ) group = parser.add_argument_group( "Replacements", diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index b59748072..e641546f3 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -3,7 +3,7 @@ import os import re from netlib import http_auth, certutils from .. import utils, platform, version -from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode +from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode, Socks5ProxyMode TRANSPARENT_SSL_PORTS = [443, 8443] CONF_BASENAME = "mitmproxy" @@ -31,6 +31,8 @@ class ProxyConfig: if mode == "transparent": self.mode = TransparentProxyMode(platform.resolver(), TRANSPARENT_SSL_PORTS) + elif mode == "socks5": + self.mode = Socks5ProxyMode(TRANSPARENT_SSL_PORTS) elif mode == "reverse": self.mode = ReverseProxyMode(upstream_server) elif mode == "upstream": @@ -63,6 +65,9 @@ def process_proxy_options(parser, options): if not platform.resolver: return parser.error("Transparent mode not supported on this platform.") mode = "transparent" + if options.socks_proxy: + c += 1 + mode = "socks5" if options.reverse_proxy: c += 1 mode = "reverse" @@ -72,7 +77,7 @@ def process_proxy_options(parser, options): mode = "upstream" upstream_server = options.upstream_proxy if c > 1: - return parser.error("Transparent mode, reverse mode and upstream proxy mode " + return parser.error("Transparent, SOCKS5, reverse and upstream proxy mode " "are mutually exclusive.") if options.clientcerts: diff --git a/libmproxy/proxy/primitives.py b/libmproxy/proxy/primitives.py index 23d089d37..c0ae424df 100644 --- a/libmproxy/proxy/primitives.py +++ b/libmproxy/proxy/primitives.py @@ -1,5 +1,5 @@ from __future__ import absolute_import - +from netlib import socks class ProxyError(Exception): def __init__(self, code, message, headers=None): @@ -15,7 +15,7 @@ class ProxyMode(object): http_form_in = None http_form_out = None - def get_upstream_server(self, conn): + def get_upstream_server(self, client_conn): """ Returns the address of the server to connect to. Returns None if the address needs to be determined on the protocol level (regular proxy mode) @@ -46,7 +46,7 @@ class RegularProxyMode(ProxyMode): http_form_in = "absolute" http_form_out = "relative" - def get_upstream_server(self, conn): + def get_upstream_server(self, client_conn): return None @@ -58,9 +58,9 @@ class TransparentProxyMode(ProxyMode): self.resolver = resolver self.sslports = sslports - def get_upstream_server(self, conn): + def get_upstream_server(self, client_conn): try: - dst = self.resolver.original_addr(conn) + dst = self.resolver.original_addr(client_conn.connection) except Exception, e: raise ProxyError(502, "Transparent mode failure: %s" % str(e)) @@ -71,11 +71,80 @@ class TransparentProxyMode(ProxyMode): return [ssl, ssl] + list(dst) +class Socks5ProxyMode(ProxyMode): + http_form_in = "relative" + http_form_out = "relative" + + def __init__(self, sslports): + self.sslports = sslports + + @staticmethod + def _assert_socks5(msg): + if msg.ver != socks.VERSION.SOCKS5: + if msg.ver == ord("G") and len(msg.methods) == ord("E"): + guess = "Probably not a SOCKS request but a regular HTTP request. " + else: + guess = "" + raise socks.SocksError( + socks.REP.GENERAL_SOCKS_SERVER_FAILURE, + guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % msg.ver) + + def get_upstream_server(self, client_conn): + try: + # Parse Client Greeting + client_greet = socks.ClientGreeting.from_file(client_conn.rfile) + self._assert_socks5(client_greet) + if socks.METHOD.NO_AUTHENTICATION_REQUIRED not in client_greet.methods: + raise socks.SocksError( + socks.METHOD.NO_ACCEPTABLE_METHODS, + "mitmproxy only supports SOCKS without authentication" + ) + + # Send Server Greeting + server_greet = socks.ServerGreeting( + socks.VERSION.SOCKS5, + socks.METHOD.NO_AUTHENTICATION_REQUIRED + ) + server_greet.to_file(client_conn.wfile) + client_conn.wfile.flush() + + # Parse Connect Request + connect_request = socks.Message.from_file(client_conn.rfile) + self._assert_socks5(connect_request) + if connect_request.msg != socks.CMD.CONNECT: + raise socks.SocksError( + socks.REP.COMMAND_NOT_SUPPORTED, + "mitmproxy only supports SOCKS5 CONNECT." + ) + + # We do not connect here yet, as the clientconnect event has not been handled yet. + + connect_reply = socks.Message( + socks.VERSION.SOCKS5, + socks.REP.SUCCEEDED, + socks.ATYP.DOMAINNAME, + client_conn.address # dummy value, we don't have an upstream connection yet. + ) + connect_reply.to_file(client_conn.wfile) + client_conn.wfile.flush() + + ssl = bool(connect_request.addr.port in self.sslports) + return ssl, ssl, connect_request.addr.host, connect_request.addr.port + + except socks.SocksError as e: + msg = socks.Message(5, e.code, socks.ATYP.DOMAINNAME, repr(e)) + try: + msg.to_file(client_conn.wfile) + except: + pass + raise ProxyError(502, "SOCKS5 mode failure: %s" % str(e)) + + class _ConstDestinationProxyMode(ProxyMode): def __init__(self, dst): self.dst = dst - def get_upstream_server(self, conn): + def get_upstream_server(self, client_conn): return self.dst diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 0152f5399..57932b0f0 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -70,7 +70,7 @@ class ConnectionHandler: # Can we already identify the target server and connect to it? client_ssl, server_ssl = False, False - upstream_info = self.config.mode.get_upstream_server(self.client_conn.connection) + upstream_info = self.config.mode.get_upstream_server(self.client_conn) if upstream_info: self.set_server_address(upstream_info[2:]) client_ssl, server_ssl = upstream_info[:2] From d0809a210b8269ffc8e3e6808403b934671e625e Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 9 Oct 2014 02:47:32 +0200 Subject: [PATCH 07/89] fix cert forwarding --- libmproxy/proxy/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 57932b0f0..4c7fbbf05 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -234,7 +234,7 @@ class ConnectionHandler: def find_cert(self): if self.config.certforward and self.server_conn.ssl_established: - return self.server_conn.cert, self.config.certstore.gen_pkey(self.server_conn.cert) + return self.server_conn.cert, self.config.certstore.gen_pkey(self.server_conn.cert), None else: host = self.server_conn.address.host sans = [] From d7341e77986fb944ebdac629424f677eb923a79f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 17 Oct 2014 17:08:41 +0200 Subject: [PATCH 08/89] add test case for #375 --- test/tools/passive_close.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 test/tools/passive_close.py diff --git a/test/tools/passive_close.py b/test/tools/passive_close.py new file mode 100644 index 000000000..d0b36e7f7 --- /dev/null +++ b/test/tools/passive_close.py @@ -0,0 +1,21 @@ +import SocketServer +from threading import Thread +from time import sleep + +class service(SocketServer.BaseRequestHandler): + def handle(self): + data = 'dummy' + print "Client connected with ", self.client_address + while True: + self.request.send("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\ncontent") + data = self.request.recv(1024) + if not len(data): + print "Connection closed by remote: ", self.client_address + sleep(3600) + + +class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + pass + +server = ThreadedTCPServer(('',1520), service) +server.serve_forever() From 52b29d49264e1397db6c65ee773479391b3fd37a Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 18 Oct 2014 15:26:10 +0200 Subject: [PATCH 09/89] remove default_ca --- examples/flowbasic | 2 +- examples/stickycookies | 5 +---- libmproxy/cmdline.py | 2 +- libmproxy/proxy/config.py | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/flowbasic b/examples/flowbasic index b99266c88..41402b0cc 100755 --- a/examples/flowbasic +++ b/examples/flowbasic @@ -36,7 +36,7 @@ class MyMaster(flow.FlowMaster): config = proxy.ProxyConfig( port=8080, - default_ca=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem") + confdir="~/.mitmproxy/" # use ~/.mitmproxy/mitmproxy-ca.pem as default CA file. ) state = flow.State() server = ProxyServer(config) diff --git a/examples/stickycookies b/examples/stickycookies index 94adfcf80..67b31da1d 100755 --- a/examples/stickycookies +++ b/examples/stickycookies @@ -36,10 +36,7 @@ class StickyMaster(controller.Master): flow.reply() -config = proxy.ProxyConfig( - port=8080, - default_ca=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem") -) +config = proxy.ProxyConfig(port=8080) server = ProxyServer(config) m = StickyMaster(server) m.run() diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index fe68e95ee..c0eb57c97 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -189,7 +189,7 @@ def common_options(parser): parser.add_argument( "--confdir", action="store", type=str, dest="confdir", default='~/.mitmproxy', - help="Configuration directory. (~/.mitmproxy)" + help="Configuration directory, contains default CA file. (~/.mitmproxy)" ) parser.add_argument( "--host", diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index e641546f3..abdb7c415 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -16,7 +16,7 @@ def parse_host_pattern(patterns): class ProxyConfig: def __init__(self, host='', port=8080, server_version=version.NAMEVERSION, - confdir=CONF_DIR, default_ca=None, clientcerts=None, + confdir=CONF_DIR, clientcerts=None, no_upstream_cert=False, body_size_limit=None, mode=None, upstream_server=None, http_form_in=None, http_form_out=None, authenticator=None, ignore=[], @@ -47,7 +47,6 @@ class ProxyConfig: self.ignore = parse_host_pattern(ignore) self.authenticator = authenticator self.confdir = os.path.expanduser(confdir) - self.default_ca = default_ca or os.path.join(self.confdir, CONF_BASENAME + "-ca.pem") self.certstore = certutils.CertStore.from_store(self.confdir, CONF_BASENAME) for spec, cert in certs: self.certstore.add_cert_file(spec, cert) From e1148584380058f264b7aa7e9493115e4e8f2bbe Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 18 Oct 2014 18:29:35 +0200 Subject: [PATCH 10/89] add generic tcp proxying, fix #374 --- doc-src/_nav.html | 1 + doc-src/features/index.py | 1 + doc-src/features/passthrough.html | 12 ++-- doc-src/features/responsestreaming.html | 6 +- doc-src/features/tcpproxy.html | 30 +++++++++ libmproxy/cmdline.py | 9 ++- libmproxy/console/__init__.py | 30 +++++++-- libmproxy/console/grideditor.py | 4 +- libmproxy/console/help.py | 1 + libmproxy/flow.py | 18 +++-- libmproxy/protocol/http.py | 9 ++- libmproxy/protocol/primitives.py | 6 +- libmproxy/protocol/tcp.py | 35 +++++----- libmproxy/proxy/config.py | 27 ++++++-- libmproxy/proxy/server.py | 20 +++--- libmproxy/web/static/flows.json | 28 ++++---- test/test_flow.py | 12 ++-- test/test_server.py | 89 ++++++++++++++++++++++--- 18 files changed, 250 insertions(+), 88 deletions(-) create mode 100644 doc-src/features/tcpproxy.html diff --git a/doc-src/_nav.html b/doc-src/_nav.html index 822e9fa6d..8bd03db2d 100644 --- a/doc-src/_nav.html +++ b/doc-src/_nav.html @@ -17,6 +17,7 @@ $!nav("serverreplay.html", this, state)!$ $!nav("setheaders.html", this, state)!$ $!nav("passthrough.html", this, state)!$ + $!nav("tcpproxy.html", this, state)!$ $!nav("sticky.html", this, state)!$ $!nav("reverseproxy.html", this, state)!$ $!nav("upstreamproxy.html", this, state)!$ diff --git a/doc-src/features/index.py b/doc-src/features/index.py index 477bb8af0..40a2669cc 100644 --- a/doc-src/features/index.py +++ b/doc-src/features/index.py @@ -12,6 +12,7 @@ pages = [ Page("setheaders.html", "Set Headers"), Page("serverreplay.html", "Server-side replay"), Page("sticky.html", "Sticky cookies and auth"), + Page("tcpproxy.html", "TCP Proxy"), Page("upstreamcerts.html", "Upstream Certs"), Page("upstreamproxy.html", "Upstream proxy mode"), ] \ No newline at end of file diff --git a/doc-src/features/passthrough.html b/doc-src/features/passthrough.html index 039d6b588..7c830639d 100644 --- a/doc-src/features/passthrough.html +++ b/doc-src/features/passthrough.html @@ -1,13 +1,12 @@ -There are a couple of reasons why you may want to exempt some traffic from mitmproxy's interception mechanism: +There are two main reasons why you may want to exempt some traffic from mitmproxy's interception mechanism: - **Certificate pinning:** Some traffic is is protected using [certificate pinning](https://security.stackexchange.com/questions/29988/what-is-certificate-pinning) and mitmproxy's interception leads to errors. For example, Windows Update or the Apple App Store fail to work if mitmproxy is active. -- **Non-HTTP traffic:** WebSockets or other non-http protocols are not supported by mitmproxy yet. You can exempt the - domain from processing, which would otherwise fail. - **Convenience:** You really don't care about some parts of the traffic and just want them to go away. -If you want to ignore traffic from mitmproxy's processing because of large response bodies, check out the +If you want to peek into (SSL-protected) non-HTTP connections, check out the [tcp proxy](@!urlTo("tcpproxy.html")!@) feature. +If you want to ignore traffic from mitmproxy's processing because of large response bodies, take a look at the [response streaming](@!urlTo("responsestreaming.html")!@) feature. ## How it works @@ -74,4 +73,9 @@ Here are some other examples for ignore patterns: --ignore 17\.178\.\d+\.\d+:443 +### See Also + +- [TCP Proxy](@!urlTo("tcpproxy.html")!@) +- [Response Streaming](@!urlTo("responsestreaming.html")!@) + [^explicithttp]: This stems from an limitation of explicit HTTP proxying: A single connection can be re-used for multiple target domains - a GET http://example.com/ request may be followed by a GET http://evil.com/ request on the same connection. If we start to ignore the connection after the first request, we would miss the relevant second one. \ No newline at end of file diff --git a/doc-src/features/responsestreaming.html b/doc-src/features/responsestreaming.html index d20af65c3..47fafef7f 100644 --- a/doc-src/features/responsestreaming.html +++ b/doc-src/features/responsestreaming.html @@ -47,4 +47,8 @@ When response streaming is enabled, portions of the code which would have otherw on the response body will see an empty response body instead (libmproxy.protocol.http.CONTENT_MISSING). Any modifications will be ignored. Streamed responses are usually sent in chunks of 4096 bytes. If the response is sent with a Transfer-Encoding: - chunked header, the response will be streamed one chunk at a time. \ No newline at end of file + chunked header, the response will be streamed one chunk at a time. + +### See Also + +- [Ignore Domains](@!urlTo("passthrough.html")!@) diff --git a/doc-src/features/tcpproxy.html b/doc-src/features/tcpproxy.html new file mode 100644 index 000000000..819cf297a --- /dev/null +++ b/doc-src/features/tcpproxy.html @@ -0,0 +1,30 @@ +WebSockets or other non-HTTP protocols are not supported by mitmproxy yet. However, you can exempt hostnames from +processing, so that mitmproxy acts as a generic TCP forwarder. This feature is closely related to the +[ignore domains](@!urlTo("passthrough.html")!@) functionality, but differs in two important aspects: + +- The raw TCP messages are printed to the event log. +- SSL connections will be intercepted. + +Please note that message interception or modification are not possible yet. +If you are not interested in the raw TCP messages, you should use the ignore domains feature. + +## How it works + + + + + + + + + + + +
command-line --tcp HOST
mitmproxy shortcut T
+ +For a detailed description on the structure of the hostname pattern, please refer to the [Ignore Domains](@!urlTo("passthrough.html")!@) feature. + +### See Also + +- [Ignore Domains](@!urlTo("passthrough.html")!@) +- [Response Streaming](@!urlTo("responsestreaming.html")!@) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index c0eb57c97..83eab7eef 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -263,7 +263,7 @@ def common_options(parser): ) group.add_argument( "-I", "--ignore", - action="append", type=str, dest="ignore", default=[], + action="append", type=str, dest="ignore_hosts", default=[], metavar="HOST", help="Ignore host and forward all traffic without processing it. " "In transparent mode, it is recommended to use an IP address (range), not the hostname. " @@ -271,6 +271,13 @@ def common_options(parser): "The supplied value is interpreted as a regular expression and matched on the ip or the hostname. " "Can be passed multiple times. " ) + group.add_argument( + "--tcp", + action="append", type=str, dest="tcp_hosts", default=[], + metavar="HOST", + help="Generic TCP SSL proxy mode for all hosts that match the pattern. Similar to --ignore," + "but SSL connections are intercepted. The communication contents are printed to the event log in verbose mode." + ) group.add_argument( "-n", action="store_true", dest="no_server", diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 9c4b4827d..cb6a977f1 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -129,10 +129,14 @@ class StatusBar(common.WWrap): r.append(":%s in file]"%self.master.server_playback.count()) else: r.append(":%s to go]"%self.master.server_playback.count()) - if self.master.get_ignore(): + if self.master.get_ignore_filter(): r.append("[") r.append(("heading_key", "I")) - r.append("gnore:%d]"%len(self.master.get_ignore())) + r.append("gnore:%d]" % len(self.master.get_ignore_filter())) + if self.master.get_tcp_filter(): + r.append("[") + r.append(("heading_key", "T")) + r.append("CP:%d]" % len(self.master.get_tcp_filter())) if self.master.state.intercept_txt: r.append("[") r.append(("heading_key", "i")) @@ -798,9 +802,13 @@ class ConsoleMaster(flow.FlowMaster): for command in commands: self.load_script(command) - def edit_ignore(self, ignore): + def edit_ignore_filter(self, ignore): patterns = (x[0] for x in ignore) - self.set_ignore(patterns) + self.set_ignore_filter(patterns) + + def edit_tcp_filter(self, tcp): + patterns = (x[0] for x in tcp) + self.set_tcp_filter(patterns) def loop(self): changed = True @@ -860,10 +868,18 @@ class ConsoleMaster(flow.FlowMaster): ) elif k == "I": self.view_grideditor( - grideditor.IgnoreEditor( + grideditor.HostPatternEditor( self, - [[x] for x in self.get_ignore()], - self.edit_ignore + [[x] for x in self.get_ignore_filter()], + self.edit_ignore_filter + ) + ) + elif k == "T": + self.view_grideditor( + grideditor.HostPatternEditor( + self, + [[x] for x in self.get_tcp_filter()], + self.edit_tcp_filter ) ) elif k == "i": diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index d629ec82e..1673d536b 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -495,8 +495,8 @@ class ScriptEditor(GridEditor): return str(v) -class IgnoreEditor(GridEditor): - title = "Editing ignore patterns" +class HostPatternEditor(GridEditor): + title = "Editing host patterns" columns = 1 headings = ("Regex (matched on hostname:port / ip:port)",) diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index bdcf3fd97..27288a369 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -119,6 +119,7 @@ class HelpView(urwid.ListBox): ("s", "add/remove scripts"), ("S", "server replay"), ("t", "set sticky cookie expression"), + ("T", "set tcp proxying pattern"), ("u", "set sticky auth expression"), ] text.extend(common.format_keyvals(keys, key="key", val="text", indent=4)) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 440798bc2..5c3a0c7e4 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -11,7 +11,7 @@ import netlib.http from . import controller, protocol, tnetstring, filt, script, version from .onboarding import app from .protocol import http, handle -from .proxy.config import parse_host_pattern +from .proxy.config import HostMatcher import urlparse ODict = odict.ODict @@ -515,11 +515,17 @@ class FlowMaster(controller.Master): for script in self.scripts: self.run_single_script_hook(script, name, *args, **kwargs) - def get_ignore(self): - return [i.pattern for i in self.server.config.ignore] + def get_ignore_filter(self): + return self.server.config.check_ignore.patterns - def set_ignore(self, ignore): - self.server.config.ignore = parse_host_pattern(ignore) + def set_ignore_filter(self, host_patterns): + self.server.config.check_ignore = HostMatcher(host_patterns) + + def get_tcp_filter(self): + return self.server.config.check_tcp.patterns + + def set_tcp_filter(self, host_patterns): + self.server.config.check_tcp = HostMatcher(host_patterns) def set_stickycookie(self, txt): if txt: @@ -787,7 +793,7 @@ class FlowReader: v = ".".join(str(i) for i in data["version"]) raise FlowReadError("Incompatible serialized data version: %s"%v) off = self.fo.tell() - yield handle.protocols[data["conntype"]]["flow"].from_state(data) + yield handle.protocols[data["type"]]["flow"].from_state(data) except ValueError, v: # Error is due to EOF if self.fo.tell() == off and self.fo.read() == '': diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 32a88b4b9..33d860ca8 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1260,9 +1260,9 @@ class HTTPHandler(ProtocolHandler): Returns False, if the connection should be closed immediately. """ address = tcp.Address.wrap(address) - if self.c.check_ignore_address(address): + if self.c.config.check_ignore(address): self.c.log("Ignore host: %s:%s" % address(), "info") - TCPHandler(self.c).handle_messages() + TCPHandler(self.c, log=False).handle_messages() return False else: self.expected_form_in = "relative" @@ -1274,6 +1274,11 @@ class HTTPHandler(ProtocolHandler): self.c.establish_ssl(server=True, client=True) self.c.log("Upgrade to SSL completed.", "debug") + if self.c.config.check_tcp(address): + self.c.log("Generic TCP mode for host: %s:%s" % address(), "info") + TCPHandler(self.c).handle_messages() + return False + return True def authenticate(self, request): diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py index 519693db5..1bf7f832d 100644 --- a/libmproxy/protocol/primitives.py +++ b/libmproxy/protocol/primitives.py @@ -59,8 +59,8 @@ class Flow(stateobject.StateObject): A Flow is a collection of objects representing a single transaction. This class is usually subclassed for each protocol, e.g. HTTPFlow. """ - def __init__(self, conntype, client_conn, server_conn, live=None): - self.conntype = conntype + def __init__(self, type, client_conn, server_conn, live=None): + self.type = type self.id = str(uuid.uuid4()) self.client_conn = client_conn """@type: ClientConnection""" @@ -78,7 +78,7 @@ class Flow(stateobject.StateObject): error=Error, client_conn=ClientConnection, server_conn=ServerConnection, - conntype=str + type=str ) def get_state(self, short=False): diff --git a/libmproxy/protocol/tcp.py b/libmproxy/protocol/tcp.py index a56bf07b3..da0c9087c 100644 --- a/libmproxy/protocol/tcp.py +++ b/libmproxy/protocol/tcp.py @@ -13,6 +13,10 @@ class TCPHandler(ProtocolHandler): chunk_size = 4096 + def __init__(self, c, log=True): + super(TCPHandler, self).__init__(c) + self.log = log + def handle_messages(self): self.c.establish_server_connection() @@ -63,26 +67,25 @@ class TCPHandler(ProtocolHandler): # if one of the peers is over SSL, we need to send # bytes/strings if not src.ssl_established: - # only ssl to dst, i.e. we revc'd into buf but need - # bytes/string now. + # we revc'd into buf but need bytes/string now. contents = buf[:size].tobytes() - self.c.log( - "%s %s\r\n%s" % ( - direction, dst_str, cleanBin(contents) - ), - "debug" - ) + if self.log: + self.c.log( + "%s %s\r\n%s" % ( + direction, dst_str, cleanBin(contents) + ), + "info" + ) dst.connection.send(contents) else: # socket.socket.send supports raw bytearrays/memoryviews - self.c.log( - "%s %s\r\n%s" % ( - direction, - dst_str, - cleanBin(buf.tobytes()) - ), - "debug" - ) + if self.log: + self.c.log( + "%s %s\r\n%s" % ( + direction, dst_str, cleanBin(buf.tobytes()) + ), + "info" + ) dst.connection.send(buf[:size]) except socket.error as e: self.c.log("TCP connection closed unexpectedly.", "debug") diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index abdb7c415..948decc11 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import os import re -from netlib import http_auth, certutils +from netlib import http_auth, certutils, tcp from .. import utils, platform, version from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode, Socks5ProxyMode @@ -10,8 +10,21 @@ CONF_BASENAME = "mitmproxy" CONF_DIR = "~/.mitmproxy" -def parse_host_pattern(patterns): - return [re.compile(p, re.IGNORECASE) for p in patterns] +class HostMatcher(object): + def __init__(self, patterns=[]): + self.patterns = list(patterns) + self.regexes = [re.compile(p, re.IGNORECASE) for p in self.patterns] + + def __call__(self, address): + address = tcp.Address.wrap(address) + host = "%s:%s" % (address.host, address.port) + if any(rex.search(host) for rex in self.regexes): + return True + else: + return False + + def __nonzero__(self): + return bool(self.patterns) class ProxyConfig: @@ -19,7 +32,7 @@ class ProxyConfig: confdir=CONF_DIR, clientcerts=None, no_upstream_cert=False, body_size_limit=None, mode=None, upstream_server=None, http_form_in=None, http_form_out=None, - authenticator=None, ignore=[], + authenticator=None, ignore_hosts=[], tcp_hosts=[], ciphers=None, certs=[], certforward=False, ssl_ports=TRANSPARENT_SSL_PORTS): self.host = host self.port = port @@ -44,7 +57,8 @@ class ProxyConfig: self.mode.http_form_in = http_form_in or self.mode.http_form_in self.mode.http_form_out = http_form_out or self.mode.http_form_out - self.ignore = parse_host_pattern(ignore) + self.check_ignore = HostMatcher(ignore_hosts) + self.check_tcp = HostMatcher(tcp_hosts) self.authenticator = authenticator self.confdir = os.path.expanduser(confdir) self.certstore = certutils.CertStore.from_store(self.confdir, CONF_BASENAME) @@ -124,7 +138,8 @@ def process_proxy_options(parser, options): upstream_server=upstream_server, http_form_in=options.http_form_in, http_form_out=options.http_form_out, - ignore=options.ignore, + ignore_hosts=options.ignore_hosts, + tcp_hosts=options.tcp_hosts, authenticator=authenticator, ciphers=options.ciphers, certs=certs, diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 4c7fbbf05..fdf6405ae 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -70,13 +70,15 @@ class ConnectionHandler: # Can we already identify the target server and connect to it? client_ssl, server_ssl = False, False + conn_kwargs = dict() upstream_info = self.config.mode.get_upstream_server(self.client_conn) if upstream_info: self.set_server_address(upstream_info[2:]) client_ssl, server_ssl = upstream_info[:2] - if self.check_ignore_address(self.server_conn.address): + if self.config.check_ignore(self.server_conn.address): self.log("Ignore host: %s:%s" % self.server_conn.address(), "info") self.conntype = "tcp" + conn_kwargs["log"] = False client_ssl, server_ssl = False, False else: pass # No upstream info from the metadata: upstream info in the protocol (e.g. HTTP absolute-form) @@ -90,15 +92,19 @@ class ConnectionHandler: if client_ssl or server_ssl: self.establish_ssl(client=client_ssl, server=server_ssl) + if self.config.check_tcp(self.server_conn.address): + self.log("Generic TCP mode for host: %s:%s" % self.server_conn.address(), "info") + self.conntype = "tcp" + # Delegate handling to the protocol handler - protocol_handler(self.conntype)(self).handle_messages() + protocol_handler(self.conntype)(self, **conn_kwargs).handle_messages() self.del_server_connection() self.log("clientdisconnect", "info") self.channel.tell("clientdisconnect", self) except ProxyError as e: - protocol_handler(self.conntype)(self).handle_error(e) + protocol_handler(self.conntype)(self, **conn_kwargs).handle_error(e) except Exception: import traceback, sys @@ -119,14 +125,6 @@ class ConnectionHandler: self.server_conn = None self.sni = None - def check_ignore_address(self, address): - address = tcp.Address.wrap(address) - host = "%s:%s" % (address.host, address.port) - if host and any(rex.search(host) for rex in self.config.ignore): - return True - else: - return False - def set_server_address(self, address): """ Sets a new server address with the given priority. diff --git a/libmproxy/web/static/flows.json b/libmproxy/web/static/flows.json index a0358db0f..35accd380 100644 --- a/libmproxy/web/static/flows.json +++ b/libmproxy/web/static/flows.json @@ -93,7 +93,7 @@ "clientcert": null, "ssl_established": true }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -259,7 +259,7 @@ "clientcert": null, "ssl_established": true }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -425,7 +425,7 @@ "clientcert": null, "ssl_established": true }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -595,7 +595,7 @@ "clientcert": null, "ssl_established": true }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -765,7 +765,7 @@ "clientcert": null, "ssl_established": true }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -919,7 +919,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1057,7 +1057,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1195,7 +1195,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1329,7 +1329,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1483,7 +1483,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1633,7 +1633,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1767,7 +1767,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -1901,7 +1901,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 @@ -2027,7 +2027,7 @@ "clientcert": null, "ssl_established": false }, - "conntype": "http", + "type": "http", "version": [ 0, 11 diff --git a/test/test_flow.py b/test/test_flow.py index b74119ddc..f08445368 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -5,8 +5,8 @@ import mock from libmproxy import filt, protocol, controller, utils, tnetstring, flow from libmproxy.protocol.primitives import Error, Flow from libmproxy.protocol.http import decoded, CONTENT_MISSING -from libmproxy.proxy.connection import ClientConnection, ServerConnection -from netlib import tcp +from libmproxy.proxy.connection import ClientConnection +from libmproxy.proxy.config import HostMatcher import tutils @@ -584,11 +584,11 @@ class TestFlowMaster: def test_getset_ignore(self): p = mock.Mock() - p.config.ignore = [] + p.config.check_ignore = HostMatcher() fm = flow.FlowMaster(p, flow.State()) - assert not fm.get_ignore() - fm.set_ignore(["^apple\.com:", ":443$"]) - assert fm.get_ignore() + assert not fm.get_ignore_filter() + fm.set_ignore_filter(["^apple\.com:", ":443$"]) + assert fm.get_ignore_filter() def test_replay(self): s = flow.State() diff --git a/test/test_server.py b/test/test_server.py index 0ce5d056d..6035b3a43 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -1,5 +1,5 @@ import socket, time -from libmproxy.proxy.config import parse_host_pattern +from libmproxy.proxy.config import HostMatcher from netlib import tcp, http_auth, http from libpathod import pathoc, pathod from netlib.certutils import SSLCert @@ -79,11 +79,14 @@ class CommonMixin: class TcpMixin: def _ignore_on(self): - ignore = parse_host_pattern([".+:%s" % self.server.port])[0] - self.config.ignore.append(ignore) + assert not hasattr(self, "_ignore_backup") + self._ignore_backup = self.config.check_ignore + self.config.check_ignore = HostMatcher([".+:%s" % self.server.port] + self.config.check_ignore.patterns) def _ignore_off(self): - self.config.ignore.pop() + assert hasattr(self, "_ignore_backup") + self.config.check_ignore = self._ignore_backup + del self._ignore_backup def test_ignore(self): spec = '304:h"Alternate-Protocol"="mitmproxy-will-remove-this"' @@ -114,6 +117,40 @@ class TcpMixin: tutils.raises("invalid server response", self.pathod, spec) # pathoc tries to parse answer as HTTP self._ignore_off() + def _tcpproxy_on(self): + assert not hasattr(self, "_tcpproxy_backup") + self._tcpproxy_backup = self.config.check_tcp + self.config.check_tcp = HostMatcher([".+:%s" % self.server.port] + self.config.check_tcp.patterns) + + def _tcpproxy_off(self): + assert hasattr(self, "_tcpproxy_backup") + self.config.check_ignore = self._tcpproxy_backup + del self._tcpproxy_backup + + + def test_tcp(self): + spec = '304:h"Alternate-Protocol"="mitmproxy-will-remove-this"' + n = self.pathod(spec) + self._tcpproxy_on() + i = self.pathod(spec) + i2 = self.pathod(spec) + self._tcpproxy_off() + + assert i.status_code == i2.status_code == n.status_code == 304 + assert "Alternate-Protocol" in i.headers + assert "Alternate-Protocol" in i2.headers + assert "Alternate-Protocol" not in n.headers + + # Test that we get the original SSL cert + if self.ssl: + i_cert = SSLCert(i.sslinfo.certchain[0]) + i2_cert = SSLCert(i2.sslinfo.certchain[0]) + n_cert = SSLCert(n.sslinfo.certchain[0]) + + assert i_cert == i2_cert == n_cert + + # Make sure that TCP messages are in the event log. + assert any("mitmproxy-will-remove-this" in m for m in self.master.log) class AppMixin: def test_app(self): @@ -579,16 +616,50 @@ class TestUpstreamProxy(tservers.HTTPUpstreamProxTest, CommonMixin, AppMixin): class TestUpstreamProxySSL(tservers.HTTPUpstreamProxTest, CommonMixin, TcpMixin): ssl = True + def _host_pattern_on(self, attr): + """ + Updates config.check_tcp or check_ignore, depending on attr. + """ + assert not hasattr(self, "_ignore_%s_backup" % attr) + backup = [] + for proxy in self.chain: + old_matcher = getattr(proxy.tmaster.server.config, "check_%s" % attr) + backup.append(old_matcher) + setattr( + proxy.tmaster.server.config, + "check_%s" % attr, + HostMatcher([".+:%s" % self.server.port] + old_matcher.patterns) + ) + + setattr(self, "_ignore_%s_backup" % attr, backup) + + def _host_pattern_off(self, attr): + backup = getattr(self, "_ignore_%s_backup" % attr) + for proxy in reversed(self.chain): + setattr( + proxy.tmaster.server.config, + "check_%s" % attr, + backup.pop() + ) + + assert not backup + delattr(self, "_ignore_%s_backup" % attr) + def _ignore_on(self): super(TestUpstreamProxySSL, self)._ignore_on() - ignore = parse_host_pattern([".+:%s" % self.server.port])[0] - for proxy in self.chain: - proxy.tmaster.server.config.ignore.append(ignore) + self._host_pattern_on("ignore") def _ignore_off(self): super(TestUpstreamProxySSL, self)._ignore_off() - for proxy in self.chain: - proxy.tmaster.server.config.ignore.pop() + self._host_pattern_off("ignore") + + def _tcpproxy_on(self): + super(TestUpstreamProxySSL, self)._tcpproxy_on() + self._host_pattern_on("tcp") + + def _tcpproxy_off(self): + super(TestUpstreamProxySSL, self)._tcpproxy_off() + self._host_pattern_off("tcp") def test_simple(self): p = self.pathoc() From 37cc6ae0bbb32e528435f821469d36055574a810 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 19 Oct 2014 01:26:08 +0200 Subject: [PATCH 11/89] fix race conditions in tests --- libmproxy/flow.py | 2 ++ test/test_flow.py | 3 +++ test/test_server.py | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 5c3a0c7e4..d313c94aa 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -662,6 +662,8 @@ class FlowMaster(controller.Master): """ Returns None if successful, or error message if not. """ + if f.live: + return "Can't replay request which is still live..." if f.intercepting: return "Can't replay while intercepting..." if f.request.content == http.CONTENT_MISSING: diff --git a/test/test_flow.py b/test/test_flow.py index f08445368..92c5b19d8 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -600,6 +600,9 @@ class TestFlowMaster: f.intercepting = True assert "intercepting" in fm.replay_request(f) + f.live = True + assert "live" in fm.replay_request(f) + def test_script_reqerr(self): s = flow.State() fm = flow.FlowMaster(None, s) diff --git a/test/test_server.py b/test/test_server.py index 6035b3a43..c81eab2b9 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -19,6 +19,17 @@ class CommonMixin: def test_large(self): assert len(self.pathod("200:b@50k").content) == 1024*50 + @staticmethod + def wait_until_not_live(flow): + """ + Race condition: We don't want to replay the flow while it is still live. + """ + s = time.time() + while flow.live: + time.sleep(0.001) + if time.time() - s > 5: + raise RuntimeError("Flow is live for too long.") + def test_replay(self): assert self.pathod("304").status_code == 304 if isinstance(self, tservers.HTTPUpstreamProxTest) and self.ssl: @@ -28,6 +39,7 @@ class CommonMixin: l = self.master.state.view[-1] assert l.response.code == 304 l.request.path = "/p/305" + self.wait_until_not_live(l) rt = self.master.replay_request(l, block=True) assert l.response.code == 305 From 6cef6fbfec92f1154b6a5b986548478137598975 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 21 Oct 2014 15:08:39 +0200 Subject: [PATCH 12/89] tweak SSL detection heuristics --- libmproxy/protocol/http.py | 10 +++++++++- libmproxy/proxy/config.py | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 33d860ca8..adb743a21 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1269,7 +1269,15 @@ class HTTPHandler(ProtocolHandler): self.expected_form_out = "relative" self.skip_authentication = True - if address.port in self.c.config.ssl_ports: + # In practice, nobody issues a CONNECT request to send unencrypted HTTP requests afterwards. + # If we don't delegate to TCP mode, we should always negotiate a SSL connection. + should_establish_ssl = ( + address.port in self.c.config.ssl_ports + or + not self.c.config.check_tcp(address) + ) + + if should_establish_ssl: self.c.log("Received CONNECT request to SSL port. Upgrading to SSL...", "debug") self.c.establish_ssl(server=True, client=True) self.c.log("Upgrade to SSL completed.", "debug") diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index 948decc11..fe2b45f4e 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -127,6 +127,12 @@ def process_proxy_options(parser, options): parser.error("Certificate file does not exist: %s" % parts[1]) certs.append(parts) + ssl_ports = options.ssl_ports + if options.ssl_ports != TRANSPARENT_SSL_PORTS: + # arparse appends to default value by default, strip that off. + # see http://bugs.python.org/issue16399 + ssl_ports = ssl_ports[len(TRANSPARENT_SSL_PORTS):] + return ProxyConfig( host=options.addr, port=options.port, @@ -144,6 +150,7 @@ def process_proxy_options(parser, options): ciphers=options.ciphers, certs=certs, certforward=options.certforward, + ssl_ports=ssl_ports ) @@ -180,7 +187,7 @@ def ssl_option_group(parser): help="Don't connect to upstream server to look up certificate details." ) group.add_argument( - "--ssl-port", action="append", type=int, dest="ssl_ports", default=TRANSPARENT_SSL_PORTS, + "--ssl-port", action="append", type=int, dest="ssl_ports", default=list(TRANSPARENT_SSL_PORTS), metavar="PORT", help="Can be passed multiple times. Specify destination ports which are assumed to be SSL. " "Defaults to %s." % str(TRANSPARENT_SSL_PORTS) From 3848a27d31a9b04c8114d0260c4b9a615d83c8cd Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 22 Oct 2014 13:59:47 +0200 Subject: [PATCH 13/89] fix #378 --- doc-src/ssl.html | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/doc-src/ssl.html b/doc-src/ssl.html index 91225d793..16aed1dcf 100644 --- a/doc-src/ssl.html +++ b/doc-src/ssl.html @@ -41,10 +41,26 @@ The files created by mitmproxy in the .mitmproxy directory are as follows: Using a custom certificate -------------------------- -You can use your own certificate by passing the __--cert__ option to mitmproxy. +You can use your own certificate by passing the --cert option to mitmproxy. mitmproxy then uses the provided +certificate for interception of the specified domains instead of generating a cert signed by its own CA. -The certificate file is expected to be in the PEM format. You can generate -a certificate in this format using these instructions: +The certificate file is expected to be in the PEM format. +You can include intermediary certificates right below your leaf certificate, so that you PEM file roughly looks like +this: + +
+-----BEGIN PRIVATE KEY-----
+<private key>
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+<cert>
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+<intermediary cert (optional)>
+-----END CERTIFICATE-----
+
+ +For example, you can generate a certificate in this format using these instructions:
 > openssl genrsa -out cert.key 8192

From 05590cf6c272571aa812ace321aa30573f2e125c Mon Sep 17 00:00:00 2001
From: Aldo Cortesi 
Date: Thu, 23 Oct 2014 09:44:47 +1300
Subject: [PATCH 14/89] Documentation re-org

- No longer using README.md in the rendered documentation.
- Rendered doc instrutions are now for the released version of
mitmproxy, with dev install instructions in the README.md
---
 README.mkd           | 13 +++++++++----
 doc-src/index.html   | 25 ++++++++++++++++++++++++-
 doc-src/index.py     | 13 ++++++++-----
 doc-src/install.html | 24 ------------------------
 4 files changed, 41 insertions(+), 34 deletions(-)

diff --git a/README.mkd b/README.mkd
index 495826128..d0d1f09b9 100644
--- a/README.mkd
+++ b/README.mkd
@@ -30,8 +30,10 @@ Features
 Installation
 ------------
 
+The recommended way to install mitmproxy is running 
+
+`pip install mitmproxy`
 
-The recommended way to install mitmproxy is running pip install mitmproxy.  
 For convenience, we provide binary packages on [mitmproxy.org](http://mitmproxy.org/).
 
 
@@ -49,7 +51,8 @@ Optional packages for extended content decoding:
 * [cssutils](http://cthedot.de/cssutils/) version 1.0 or newer.
 
 For convenience, all optional dependencies can be installed with 
-`pip install mitmproxy[contenviews]`
+
+`pip install mitmproxy contentviews`
 
 __mitmproxy__ is tested and developed on OSX, Linux and OpenBSD. On Windows,
 only mitmdump is supported, which does not have a graphical user interface.
@@ -60,15 +63,17 @@ Hacking
 
 The following components are needed if you plan to hack on mitmproxy:
 
-* The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py) and [pathod](http://pathod.net), version matching mitmproxy.
+* The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py) and [pathod](http://pathod.net), version matching mitmproxy. Install these with `pip install mitmproxy dev`.
 * Rendering the documentation requires [countershape](http://github.com/cortesi/countershape).
 
-For convenience, the following procedure is recommended to set up your environment:
+The following procedure is recommended to set up your dev environment:
+
 ```
 $ git clone https://github.com/mitmproxy/mitmproxy.git
 $ cd mitmproxy
 $ pip install --src . -r requirements.txt
 ```
+
 This installs the latest GitHub versions of mitmproxy, netlib and pathod into `mitmproxy/`. All other development dependencies save countershape are installed into their usual locations. 
 
 Please ensure that all patches are accompanied by matching changes in the test
diff --git a/doc-src/index.html b/doc-src/index.html
index 79687ec61..23da7223c 100644
--- a/doc-src/index.html
+++ b/doc-src/index.html
@@ -1,4 +1,27 @@
 
-@!index_contents!@
+__mitmproxy__ is an interactive, SSL-capable man-in-the-middle proxy for HTTP
+with a console interface.
+
+__mitmdump__ is the command-line version of mitmproxy. Think tcpdump for HTTP.
+
+__libmproxy__ is the library that mitmproxy and mitmdump are built on.
+
+Documentation, tutorials and distribution packages can be found on the
+mitmproxy.org website:
+
+[mitmproxy.org](http://mitmproxy.org).
 
 
+Features
+--------
+
+- Intercept HTTP requests and responses and modify them on the fly.
+- Save complete HTTP conversations for later replay and analysis.
+- Replay the client-side of an HTTP conversations.
+- Replay HTTP responses of a previously recorded server.
+- Reverse proxy mode to forward traffic to a specified server.
+- Transparent proxy mode on OSX and Linux.
+- Make scripted changes to HTTP traffic using Python.
+- SSL certificates for interception are generated on the fly.
+- And much, much more.
+
diff --git a/doc-src/index.py b/doc-src/index.py
index b7ab99952..e6064e3a4 100644
--- a/doc-src/index.py
+++ b/doc-src/index.py
@@ -1,6 +1,8 @@
-import os, sys, datetime
+import os
+import sys
+import datetime
 import countershape
-from countershape import Page, Directory, PythonModule, markup, model
+from countershape import Page, Directory, markup, model
 import countershape.template
 sys.path.insert(0, "..")
 from libmproxy import filt, version
@@ -23,18 +25,18 @@ ns.docMaintainer = "Aldo Cortesi"
 ns.docMaintainerEmail = "aldo@corte.si"
 ns.copyright = u"\u00a9 mitmproxy project, %s" % datetime.date.today().year
 
+
 def mpath(p):
     p = os.path.join(MITMPROXY_SRC, p)
     return os.path.expanduser(p)
 
-with open(mpath("README.mkd")) as f:
-        readme = f.read()
-        ns.index_contents = readme.split("\n", 1)[1] #remove first line (contains build status)
 
 def example(s):
     d = file(mpath(s)).read().rstrip()
     extemp = """
%s
(%s)
""" return extemp%(countershape.template.Syntax("py")(d), s) + + ns.example = example @@ -73,6 +75,7 @@ def nav(page, current, state): ns.nav = nav ns.navbar = countershape.template.File(None, "_nav.html") + pages = [ Page("index.html", "Introduction"), Page("install.html", "Installation"), diff --git a/doc-src/install.html b/doc-src/install.html index 5d4124597..5f1e54fd1 100644 --- a/doc-src/install.html +++ b/doc-src/install.html @@ -4,30 +4,11 @@ release or from source - is to use [pip](http://www.pip-installer.org/). If you don't already have pip on your system, you can find installation instructions [here](http://www.pip-installer.org/en/latest/installing.html). - -## Installing the latest release - -A single command will download and install the latest release of mitmproxy, -along with all its dependencies: -
 pip install mitmproxy
 
-## Installing from source - -When installing from source, the easiest method is still to use pip. In this -case run: - -
-pip install /path/to/source
-
- -Note that if you're installing current git master, you will also have to -install the current git master of [netlib](http://github.com/mitmproxy/netlib) by -hand. - ## OSX - If you're running a Python interpreter installed with homebrew (or similar), @@ -64,8 +45,3 @@ from source: - libxslt1-dev - - - - - From 4da90724a0368ddf112644d29545a6ecc92a2861 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 23 Oct 2014 12:56:31 +1300 Subject: [PATCH 15/89] First redraft of modes documentation --- doc-src/modes.html | 292 +++++++++++++++++++++++---------------------- 1 file changed, 152 insertions(+), 140 deletions(-) diff --git a/doc-src/modes.html b/doc-src/modes.html index 77bd1b056..b78fe3c09 100644 --- a/doc-src/modes.html +++ b/doc-src/modes.html @@ -1,210 +1,222 @@ -Mitmproxy comes with several modes of operation, which allow you to use mitmproxy in a variety of scenarios. -This documents briefly explains each mode and possible setups. -
-Mitmproxy has four modes of operation: -
    -
  • Regular Mode (this is what you get by default)
  • -
  • Transparent Mode
  • -
  • Reverse Proxy Mode
  • -
  • Upstream Proxy Mode
  • -
-

Now, which one should you pick? Use this flow chart: -

+Mitmproxy has four modes of operation that allow you to use mitmproxy in a +variety of scenarios: -

+- **Regular** (the default) +- **Transparent** +- **Reverse Proxy** +- **Upstream Proxy** + +Now, which one should you pick? Use this flow chart: + + -Mitmproxy's regular mode it the most simple one and the easiest to set up. +Mitmproxy's regular mode is the simplest and the easiest to set up. -
    -
  1. Start mitmproxy.
  2. -
  3. Configure your client to use mitmproxy. This means that you either adjust the proxy setting of your local browser - or point an external device to your proxy (which should look like - this).
  4. -
  5. Quick Check: You can already visit an unencrypted HTTP site over the proxy.
  6. -
  7. Open the magic domain mitm.it and install the certificate for your device.
  8. -
+1. Start mitmproxy. +2. Configure your client to use mitmproxy. For instance on IOS, the settings might look like this. +3. Quick Check: You should already be able to visit an unencrypted HTTP site +through the proxy. +4. Open the magic domain mitm.it and install the certificate for your device.
- Heads Up: Unfortunately, some applications prefer to bypass the HTTP proxy settings of the system - - Android applications are a common example. In these cases, you need to use mitmproxy's transparent mode. +Heads Up: Unfortunately, some applications bypass the +system HTTP proxy settings - Android applications are a common example. In +these cases, you need to use mitmproxy's transparent mode.
-

If you are proxying an external device, your network will probably look like this:

+If you are proxying an external device, your network will probably look like this: + -

-

The square brackets signify the source and destination IP addresses. Your client explicitly connects - to mitmproxy and mitmproxy explicitly connects to the target server. -

+ +The square brackets signify the source and destination IP addresses. Your +client explicitly connects to mitmproxy and mitmproxy explicitly connects +to the target server. -When a transparent proxy is used, traffic is redirected into a proxy at the network layer, without any client -configuration being required. This makes transparent proxying ideal for those situations where you can't change client -behaviour. The basic principle is that mitmproxy sits somewhere on the line from the client to the internet and -transparently intercepts the request. In the graphic below, a machine running mitmproxy has been inserted between -the router and the internet: +In transparent mode, traffic is directed into a proxy at the network layer, +without any client configuration required. This makes transparent proxying +ideal for situations where you can't change client behaviour. In the graphic +below, a machine running mitmproxy has been inserted between the router and +the internet: - -

The square brackets signify the source and destination IP addresses. Round brackets mark the next - hop on the Ethernet/data link layer. This distinction is important to make: When the packet arrives - at the mitmproxy machine, it must still be addressed to the target server. In other words: A simple IP redirect on - the router does not work - this would remove the target information, leaving mitmproxy unable to - determine the real destination. -

+ + + +The square brackets signify the source and destination IP addresses. Round +brackets mark the next hop on the *Ethernet/data link* layer. This distinction +is important: when the packet arrives at the mitmproxy machine, it must still +be addressed to the target server. This means that Network Address Translation +should not be applied before the traffic reaches mitmproxy, since this would +remove the target information, leaving mitmproxy unable to determine the real +destination. +

Common Configurations

-The first graphic is a little bit idealistic: Usually, you'll have your local wireless lan network and no -machines between your router and the internet. Fortunately, there are other ways to configure your network: -(a) Configuring the client to use a custom gateway/router/"next hop", (b) Implementing custom routing on the router -or (c) setting up a separate wireless network router which gets proxied. -There are of course other options, but we'll look at these three. In most cases, setting (a) is recommended due to its -ease of use. +There are many ways to configure your network for transparent proxying. We'll +look at three common scenarios: + +1. Configuring the client to use a custom gateway/router/"next hop" +2. Implementing custom routing on the router + +In most cases, the first option is recommended due to its ease of use.

(a) Custom Gateway

-

Looking at your local home network, it's clear what happens if you enter "example.com" into your address bar: After you -press enter, your OS sends a packet to your router, which then sends this to your ISP, which then sends it to some -Tier-1 carrier, which then sends it... I think you get the idea. The important part for us is the first step here: -Your machine is configured to use your router as the next hop. Your router certainly doesn't host example.com, but your -machine knows that your router will forward it upstream. On the technical level, your router probably provides a DHCP -server, which instructs all clients to use his address as the Default Gateway for connections that leave the -current subnet (your local network).

-

-How does this help us? Here comes our trick: By configuring the client to use our machine as its Gateway, all traffic -will be sent to our machine, which then forwards it to the router. This provides us with the scenario we'd like to have, -namely packets on our doorstep that are addressed for someone else: -

+One simple way to get traffic to the mitmproxy machine with the destination IP +intact, is to simply configure the client with the mitmproxy box as the +default gateway. + -Given this concept, we can set up mitmproxy: -
    -
  1. Configure your proxy machine for transparent mode.
    You can find instructions - in the Transparent Proxying section of the mitmproxy docs.
  2. -
  3. Configure your client to use your proxy machine's IP as the default gateway. This setting is usually called - Standard Gateway, Router or something along these lines - (iOS screenshot).
  4. -
  5. Quick Check: You can already visit an unencrypted HTTP site over the proxy.
  6. -
  7. Open the magic domain mitm.it and install the certificate for your device.
  8. -
+In this scenario, we would: + +- Configure the proxy machine for transparent mode. You can find instructions +in the Transparent Proxying section of the mitmproxy docs. + +- Configure the client to use the proxy machine's IP as the default gateway. +Here is what this would +look like on IOS. + +- Quick Check: At this point, you should already be able to visit an +unencrypted HTTP site over the proxy. + +- Open the magic domain mitm.it and install the certificate +for your device. + +Setting the custom gateway on clients can be automated by serving the settings +out to clients over DHCP. This lets set up an interception network where all +clients are proxied automatically, which can save time and effort. +
Troubleshooting Transparent Mode -

Wrong transparent mode configurations are a frequent source of + +

Incorrect transparent mode configurations are a frequent source of error. If it doesn't work for you, try the following things:

+
    -
  • Open mitmproxy's event log (press `e`) - can you spot clientconnect messages? - If not, the packets are not arriving at the proxy. A common source is the occurence of ICMP redirects, - which means that your machine is telling the client that there's a faster way to the internet by contacting - your router directly (see the Transparent Proxying section on how to disable them). If in doubt, - Wireshark may help you to see whether something arrives at your machine - or not. +
  • + Open mitmproxy's event log (press `e`) - do you see clientconnect + messages? If not, the packets are not arriving at the proxy. One common + cause is the occurrence of ICMP redirects, which means that your + machine is telling the client that there's a faster way to the + internet by contacting your router directly (see the + Transparent Proxying section on how to disable them). If in + doubt, Wireshark may help you + to see whether something arrives at your machine or not.
  • - Have you explicitly configured an HTTP proxy on your device? You do not need mitmproxy's transparent mode - then, just start mitmproxy normally. Explicitly setting a proxy and transparent mode contradict each other, - settle for one. Do not explicitly redirect traffic to mitmproxy anywhere except for the Gateway setting. + Make sure you have not explicitly configured an HTTP proxy on the + client. This is not needed in transparent mode.
  • Re-check the instructions in the Transparent Proxying section. Anything you missed?
+ If you encounter any other pitfalls that should be listed here, please let us know! +

(b) Custom Routing

-Custom routing is a fairly advanced setup which we'll only document briefly here. -First and foremost, it usually requires root on your router. The basic idea is to teach your router a custom routing -table that says "for requests from ip X, the proxy machine is the next gateway". +In some cases, you may need more fine-grained control of which traffic reaches +the mitmproxy instance, and which doesn't. You may, for instance, choose only +to divert traffic to some hosts into the transparent proxy. There are a huge +number of ways to accomplish this, and much will depend on the router or +packet filter you're using. In most cases, the configuration will look like +this: - - -For this setup, we expect you to have a basic understanding of networking in general. In short, you should get started -with these routing commands. The Troubleshooting part directly above this -section might be helpful for you as well. - -

(c) Separate Network

- -Setting up a separate network using a cheap router might be a viable option, too. Such a configuration mostly resembles -the idealistic graphic from the beginning (Variant 1). Take a look at the -Transparently proxify virtual machines tutorial to see how -such a network could be implemented. The troubleshooting section for custom gateways may be helpful for you, too. + + -Mitmproxy is usually used with a client that uses the proxy to access the Internet. Using reverse proxy mode, you can -use mitmproxy to represent a server: +Mitmproxy is usually used with a client that uses the proxy to access the +Internet. Using reverse proxy mode, you can use mitmproxy to act like a normal +HTTP server: - + + There are various use-cases: -
    -
  • - Say you have an internal API running at http://example.local/. You could now setup mitmproxy in - reverse proxy mode at http://debug.example.local/ and dynamically point clients to this new API endpoint, - which provides clients with the same data and you with debug information. Similarly, you could move your real server - to a different ip/port and setup mitmproxy at the original place to debug all sessions. -
  • -
  • - Say you're a web developer working on example.com (with a development version running on localhost:8000). - You can modify your hosts file so that example.com points to 127.0.0.1 and then run mitmproxy in reverse proxy - mode on port 80. You can test your app on the example.com domain and get all requests recorded in mitmproxy. -
  • -
  • - Say you have some toy project that should get SSL support. Simply setup mitmproxy with SSL termination and you're - done (mitmdump -p 443 -R https2http://localhost:80/). There are better tools for this specific task (we don't - have C performance obviously), but it's definitely a nice and very quick way to setup an SSL-speaking server. -
  • -
  • - Want to add a non-SSL-capable compression proxy in front of your server? You could even spawn a mitmproxy instance - that terminates SSL (https2http://...), point it to the compression proxy and let the compression proxy point - to a SSL-initiating mitmproxy (http2https://...), which then points to the real server. As you see, it's a fairly - flexible thing. -
  • -
-

-Please note that cloning Google by using mitmproxy -R http://google.com/ does not really work -(as in this screenshot). -This may work for the first request, but the HTML remains unchanged: As soon as the user clicks on an non-relative URL -(or downloads a non-relative image resource), they speak with Google directly again. -

-

- On another note, mitmproxy either supports an HTTP or an HTTPS upstream server, not both at the same time. You can - simply work around this by spawning a second mitmproxy instance. Each instance listens to one port and talks to one - port. -

+- Say you have an internal API running at http://example.local/. You could now +set up mitmproxy in reverse proxy mode at http://debug.example.local/ and +dynamically point clients to this new API endpoint, which provides clients +with the same data and you with debug information. Similarly, you could move +your real server to a different IP/port and set up mitmproxy at the original +place to debug all sessions. + +- Say you're a web developer working on example.com (with a development +version running on localhost:8000). You can modify your hosts file so that +example.com points to 127.0.0.1 and then run mitmproxy in reverse proxy mode +on port 80. You can test your app on the example.com domain and get all +requests recorded in mitmproxy. + +- Say you have some toy project that should get SSL support. Simply set up +mitmproxy with SSL termination and you're done (mitmdump -p 443 -R +https2http://localhost:80/). There are better tools for this specific +task, but mitmproxy is very quick and simple way to set up an SSL-speaking +server. + +- Want to add a non-SSL-capable compression proxy in front of your server? You +could even spawn a mitmproxy instance that terminates SSL (https2http://...), +point it to the compression proxy and let the compression proxy point to a +SSL-initiating mitmproxy (http2https://...), which then points to the real +server. As you see, it's a fairly flexible thing. + +Note that mitmproxy supports either an HTTP or an HTTPS upstream server, not +both at the same time. You can work around this by spawning a second mitmproxy +instance. + +
+ Caveat: Interactive Use + + +One caveat is that reverse proxy mode is often not sufficient for interactive +browsing. Consider trying to clone Google by using: + +mitmproxy -R http://google.com/ + +This works for the initial request, but the HTML served to the client remains +unchanged. As soon as the user clicks on an non-relative URL (or downloads a +non-relative image resource), traffic no longer passes through mitmproxy, and +the client connects to Google directly again. + +
+ + -

-If you want to add mitmproxy in front of a different proxy appliance, you can use mitmproxy's upstream mode. -In upstream mode, all requests are unconditionally transferred to an upstream proxy or your choice. -

+If you want to chain proxies by adding mitmproxy in front of a different proxy +appliance, you can use mitmproxy's upstream mode. In upstream mode, all +requests are unconditionally transferred to an upstream proxy or your choice. -

-mitmproxy supports both explicit HTTP and explicit HTTPS in upstream proxy mode. You could in theory chain multiple -mitmproxy instances in a row, but that doesn't make any sense in practice (i.e. outside of our tests). -

\ No newline at end of file +mitmproxy supports both explicit HTTP and explicit HTTPS in upstream proxy +mode. You could in theory chain multiple mitmproxy instances in a row, but +that doesn't make any sense in practice (i.e. outside of our tests). From 6fcd1d0ed9b714ccd93ebb3abb0ffe0d5c3d8ff0 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 23 Oct 2014 14:38:12 +1300 Subject: [PATCH 16/89] CHANGELOG and CONTRIBUTORS --- CHANGELOG | 43 +++++++++++++++++++++++++++++++++ CONTRIBUTORS | 68 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 84 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 69e7339b8..7f8896f10 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,46 @@ + +23 October 2014: mitmproxy 0.11: + + * SOCKS proxy mode + + * Data streaming for response bodies exceeding a threshold + (bradpeabody@gmail.com) + + * Ignore hosts or IP addresses, forwarding both HTTP and HTTPS traffic + untouched + + * Finer-grained control of traffic replay, including options to ignore + contents or parameters when matching flows (marcelo.glezer@gmail.com) + + * Pass arguments to inline scripts + + * Configurable size limit on HTTP request and response bodies + + * Per-domain specification of interception certificates and keys (see + --cert option) + + * Certificate forwarding, relaying upstream SSL certificates verbatim (see + --cert-forward) + + * Search and highlighting for HTTP request and response bodies in + mitmproxy console (pedro@worcel.com) + + * Transparent proxy support on Windows + + * Improved error messages and logging + + * Support for FreeBSD in transparent mode, using pf (zbrdge@gmail.com) + + * Content view mode for WBXML (davidshaw835@air-watch.com) + + * Better documentation, with a new section on proxy modes + + * Generic TCP proxy mode + + * Countless bugfixes and other small improvements + + + 28 January 2014: mitmproxy 0.10: * Support for multiple scripts and multiple script arguments diff --git a/CONTRIBUTORS b/CONTRIBUTORS index bed636fac..a9688d92f 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,51 +1,65 @@ - 854 Aldo Cortesi - 64 Maximilian Hils + 902 Aldo Cortesi + 323 Maximilian Hils 18 Henrik Nordstrom 13 Thomas Roth + 12 Pedro Worcel 11 Stephen Altamirano 10 András Veres-Szentkirályi - 8 Jason A. Novak 8 Rouli + 8 Jason A. Novak 7 Alexis Hildebrandt - 6 Pedro Worcel 5 Tomaz Muraus + 5 Brad Peabody 5 Matthias Urlichs 4 root - 4 Bryan Bishop 4 Marc Liyanage 4 Valtteri Virtanen - 3 Kyle Manna + 4 Bryan Bishop 3 Chris Neasbitt - 2 alts - 2 Heikki Hannikainen - 2 Jim Lloyd + 3 Zack B + 3 Eli Shvartsman + 3 Kyle Manna 2 Michael Frister + 2 Bennett Blodinger + 2 Jim Lloyd 2 Rob Wills - 2 Jaime Soriano Pastor 2 israel + 2 Jaime Soriano Pastor + 2 Heikki Hannikainen 2 Mark E. Haase + 2 alts + 1 davidpshaw + 1 deployable + 1 joebowbeer + 1 meeee + 1 phil plante + 1 Michael Bisbjerg + 1 Andy Smith + 1 Dan Wilbraham + 1 David Shaw + 1 Eric Entzel + 1 Felix Wolfsteller + 1 Henrik Nordström + 1 Ivaylo Popov + 1 JC + 1 Jakub Nawalaniec + 1 James Billingham + 1 Jean Regisser + 1 Kit Randel + 1 Marcelo Glezer + 1 Mathieu Mitchell + 1 Mikhail Korobov + 1 Nicolas Esteves + 1 Oleksandr Sheremet 1 Paul 1 Rich Somerfield 1 Rory McCann - 1 Felix Wolfsteller 1 Rune Halvorsen 1 Sahn Lam - 1 Eric Entzel - 1 Dan Wilbraham + 1 Seppo Yli-Olli + 1 Sergey Chipiga + 1 Steven Van Acker 1 Ulrich Petri - 1 Andy Smith + 1 Vyacheslav Bakhmutov 1 Yuangxuan Wang 1 capt8bit - 1 joebowbeer - 1 meeee - 1 James Billingham - 1 Jakub Nawalaniec - 1 JC - 1 Kit Randel - 1 phil plante - 1 Mathieu Mitchell - 1 Ivaylo Popov - 1 Henrik Nordström - 1 Michael Bisbjerg - 1 Nicolas Esteves - 1 Oleksandr Sheremet From 5aace7eed8899756799679f7667739dfb58b4dbc Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 23 Oct 2014 15:05:01 +1300 Subject: [PATCH 17/89] Keep sidebar ordering alphabetical, add SOCKS documentation --- CHANGELOG | 2 +- doc-src/_nav.html | 9 +++++---- doc-src/features/index.py | 1 + doc-src/features/socksproxy.html | 10 ++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 doc-src/features/socksproxy.html diff --git a/CHANGELOG b/CHANGELOG index 7f8896f10..c78fdccec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ 23 October 2014: mitmproxy 0.11: - * SOCKS proxy mode + * SOCKS5 proxy mode allows mitmproxy to act as a SOCKS5 proxy server * Data streaming for response bodies exceeding a threshold (bradpeabody@gmail.com) diff --git a/doc-src/_nav.html b/doc-src/_nav.html index 8bd03db2d..0ae0fa67b 100644 --- a/doc-src/_nav.html +++ b/doc-src/_nav.html @@ -17,13 +17,14 @@ $!nav("serverreplay.html", this, state)!$ $!nav("setheaders.html", this, state)!$ $!nav("passthrough.html", this, state)!$ - $!nav("tcpproxy.html", this, state)!$ - $!nav("sticky.html", this, state)!$ + $!nav("proxyauth.html", this, state)!$ $!nav("reverseproxy.html", this, state)!$ + $!nav("responsestreaming.html", this, state)!$ + $!nav("socksproxy.html", this, state)!$ + $!nav("sticky.html", this, state)!$ + $!nav("tcpproxy.html", this, state)!$ $!nav("upstreamproxy.html", this, state)!$ $!nav("upstreamcerts.html", this, state)!$ - $!nav("proxyauth.html", this, state)!$ - $!nav("responsestreaming.html", this, state)!$ diff --git a/doc-src/features/index.py b/doc-src/features/index.py index 40a2669cc..693b4439b 100644 --- a/doc-src/features/index.py +++ b/doc-src/features/index.py @@ -9,6 +9,7 @@ pages = [ Page("replacements.html", "Replacements"), Page("responsestreaming.html", "Response Streaming"), Page("reverseproxy.html", "Reverse proxy mode"), + Page("socksproxy.html", "SOCKS Mode"), Page("setheaders.html", "Set Headers"), Page("serverreplay.html", "Server-side replay"), Page("sticky.html", "Sticky cookies and auth"), diff --git a/doc-src/features/socksproxy.html b/doc-src/features/socksproxy.html new file mode 100644 index 000000000..f436cbf5e --- /dev/null +++ b/doc-src/features/socksproxy.html @@ -0,0 +1,10 @@ + +In this mode, mitmproxy acts as a SOCKS5 proxy server. + + + + + + + +
command-line --socks
From 32127f80e2bd3ee5b33610300d11abafc9c6e3ae Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 23 Oct 2014 15:43:06 +1300 Subject: [PATCH 18/89] More refactoring of installation docs - Make it clear that README.md only has the hacking installation instructions - Beef up install.html --- README.mkd | 57 ++++++++++++++++++++++++++------------------ doc-src/install.html | 16 ++++++++++++- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/README.mkd b/README.mkd index d0d1f09b9..ccc09138b 100644 --- a/README.mkd +++ b/README.mkd @@ -13,6 +13,9 @@ mitmproxy.org website: [mitmproxy.org](http://mitmproxy.org). +You can find complete directions for installing mitmproxy [here](http://mitmproxy.org/doc/install.html). + + Features -------- @@ -26,19 +29,17 @@ Features - SSL certificates for interception are generated on the fly. - And much, much more. - -Installation ------------- - -The recommended way to install mitmproxy is running - -`pip install mitmproxy` - -For convenience, we provide binary packages on [mitmproxy.org](http://mitmproxy.org/). +__mitmproxy__ is tested and developed on OSX, Linux and OpenBSD. On Windows, +only mitmdump is supported, which does not have a graphical user interface. -Requirements ------------- + +Hacking +------- + + +### Requirements + * [Python](http://www.python.org) 2.7.x. * [netlib](http://pypi.python.org/pypi/netlib), version matching mitmproxy. @@ -52,19 +53,9 @@ Optional packages for extended content decoding: For convenience, all optional dependencies can be installed with -`pip install mitmproxy contentviews` +`pip install "mitmproxy[contentviews]"` -__mitmproxy__ is tested and developed on OSX, Linux and OpenBSD. On Windows, -only mitmdump is supported, which does not have a graphical user interface. - - -Hacking -------- - -The following components are needed if you plan to hack on mitmproxy: - -* The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py) and [pathod](http://pathod.net), version matching mitmproxy. Install these with `pip install mitmproxy dev`. -* Rendering the documentation requires [countershape](http://github.com/cortesi/countershape). +### Setting up a dev environment The following procedure is recommended to set up your dev environment: @@ -76,6 +67,26 @@ $ pip install --src . -r requirements.txt This installs the latest GitHub versions of mitmproxy, netlib and pathod into `mitmproxy/`. All other development dependencies save countershape are installed into their usual locations. + +### Testing + +The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py) and [pathod](http://pathod.net), version matching mitmproxy. Install these with: + +` +pip install "mitmproxy[dev]""` + + Please ensure that all patches are accompanied by matching changes in the test suite. The project maintains 100% test coverage. + +### Docs + +Rendering the documentation requires [countershape](http://github.com/cortesi/countershape). After installation, you can render the documentation to the doc like this: + +`cshape doc-src doc` + + + + + diff --git a/doc-src/install.html b/doc-src/install.html index 5f1e54fd1..8a76e3ae9 100644 --- a/doc-src/install.html +++ b/doc-src/install.html @@ -1,4 +1,8 @@ + + +## Installing from source + The preferred way to install mitmproxy - whether you're installing the latest release or from source - is to use [pip](http://www.pip-installer.org/). If you don't already have pip on your system, you can find installation instructions @@ -8,14 +12,24 @@ don't already have pip on your system, you can find installation instructions pip install mitmproxy
+If you also want to install the optional packages AMF, protobuf and CSS +content views, do this: + +
+pip install "mitmproxy[contentviews]"
+
+ ## OSX +The easiest way to get up and running on OSX is to download the pre-built +binary packages from [mitmproxy.org](http://mitmproxy.org). If you still want +to install using pip, there are a few things to keep in mind: + - If you're running a Python interpreter installed with homebrew (or similar), you may have to install some dependencies by hand. - Make sure that XCode is installed from the App Store, and that the command-line tools have been downloaded (XCode/Preferences/Downloads). -- Now use __pip__ to do the installation, as above. There are a few bits of customization you might want to do to make mitmproxy comfortable to use on OSX. The default color scheme is optimized for a dark From 6aa05df944add1fe7b681ae6e7d6336f2ff3ae55 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 23 Oct 2014 15:50:43 +1300 Subject: [PATCH 19/89] Correct docs - we no longer support change of basic proxy mode in the console app --- doc-src/features/reverseproxy.html | 3 --- doc-src/features/upstreamproxy.html | 3 --- 2 files changed, 6 deletions(-) diff --git a/doc-src/features/reverseproxy.html b/doc-src/features/reverseproxy.html index e6de4f339..ea91fe1f3 100644 --- a/doc-src/features/reverseproxy.html +++ b/doc-src/features/reverseproxy.html @@ -9,8 +9,5 @@ mitmproxy forwards HTTP proxy requests to an upstream proxy server. command-line -R http[s]://hostname[:port] - - mitmproxy shortcut P - diff --git a/doc-src/features/upstreamproxy.html b/doc-src/features/upstreamproxy.html index 6039f4df3..bb354cd3d 100644 --- a/doc-src/features/upstreamproxy.html +++ b/doc-src/features/upstreamproxy.html @@ -9,8 +9,5 @@ mitmproxy forwards ordinary HTTP requests to an upstream server. command-line -U http://hostname[:port] - - mitmproxy shortcut U - From 6bed0764609029e9d01b1d28b7826fb37ab20d3e Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 23 Oct 2014 16:13:03 +1300 Subject: [PATCH 20/89] Document http2https and https2http --- doc-src/features/reverseproxy.html | 17 ++++++++++++++++- doc-src/features/upstreamproxy.html | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/doc-src/features/reverseproxy.html b/doc-src/features/reverseproxy.html index ea91fe1f3..1c57f0b23 100644 --- a/doc-src/features/reverseproxy.html +++ b/doc-src/features/reverseproxy.html @@ -7,7 +7,22 @@ mitmproxy forwards HTTP proxy requests to an upstream proxy server. - +
command-line -R http[s]://hostname[:port]command-line -R schema://hostname[:port]
+ +Here, **schema** is one of http, https, http2https or https2http. The latter +two extended schema specifications control the use of HTTP and HTTPS on +mitmproxy and the upstream server. You can indicate that mitmproxy should use +HTTP, and the upstream server uses HTTPS like this: + + http2https://hostname:port + +And you can indicate that mitmproxy should use HTTPS while the upstream +service uses HTTP like this: + + https2http://hostname:port + + + diff --git a/doc-src/features/upstreamproxy.html b/doc-src/features/upstreamproxy.html index bb354cd3d..47bc115da 100644 --- a/doc-src/features/upstreamproxy.html +++ b/doc-src/features/upstreamproxy.html @@ -11,3 +11,17 @@ mitmproxy forwards ordinary HTTP requests to an upstream server. + +Here, **schema** is one of http, https, http2https or https2http. The latter +two extended schema specifications control the use of HTTP and HTTPS on +mitmproxy and the upstream server. You can indicate that mitmproxy should use +HTTP, and the upstream server uses HTTPS like this: + + http2https://hostname:port + +And you can indicate that mitmproxy should use HTTPS while the upstream +service uses HTTP like this: + + https2http://hostname:port + + From bbee391a478c208861703e73339ab55935c07c5f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 23 Oct 2014 16:14:04 +0200 Subject: [PATCH 21/89] update proxy modes diagrams --- .../schematics/proxy-modes-transparent-1.png | Bin 16305 -> 14558 bytes .../schematics/proxy-modes-transparent-2.png | Bin 23041 -> 23375 bytes .../proxy-modes-transparent-wrong.png | Bin 17568 -> 14719 bytes doc-src/schematics/proxy-modes.vsdx | Bin 195186 -> 190788 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc-src/schematics/proxy-modes-transparent-1.png b/doc-src/schematics/proxy-modes-transparent-1.png index c20274326db51c5d6b6ba5085bc067499d9f536e..002e0e76a48ad1adba70bafc245b29ab407b86a2 100644 GIT binary patch literal 14558 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

b~9pQw?kcTeaW8BL9iJrNNp?Xo8SSCH$gPf`>Y z0-tu@o@hs2w;h(x<4ScV1Jgt*M$eDub!A3O?x3D#!K(tzt5ih#qWH>eRKTnUr`VAm&@hF7rmti zCws9u#3C)`M?$+5VZ`*bVI8_zH_E?^bhPQ#hyZ%-3HZ7DV6q<7mVfKVw>rlRbhH)G zB*Y+tAM)M#G-QNb9n9sMXR1xb`0Rf?AFWSViy7WG*CYlLo69NN0So-qt5>$RwjGTd zv&}m@B@e+qdwV|`d*1;RdEq%Cbww+_OEgABw#)^brRl(hZGG7{ol`U z^JUwOC>vxilb`}M${HK_4`8wU&)l7zoW9i6`2-(MOA(YESB!4U9*QYQ@D$6j4gx0> za5a`jwbg^6rS)O}BLfVv{PG*pjs*2LhM;b8h5YDVLWKMfF{}mV_XE3eH?P`cuwWqn zf<_UBw|66|a;?-q$-ImTZ+IB%?K*JtNGfx^-=&`S`zWuC{>+`=J8}r8L z!guWXIwilq`!4}cSJ(0a{pZi=Cbs!hLxI%-Y=U+yYQ?zw<)O}nSHKADQK4VdcOG5C zby6ePj|{G|-HXRWOEJnY0j-!^<8sX^2QQ4RQ$0zi?rCgND7uN%&cjvS4n!3om0n2Y`{mDDA-o&2P92z$4D`dRh492=RrZL82*8V321){3C;Zy*`Y4tk z5w4%C*Ah!hOKZK+X~PN@){WK`M&0HjF5eMKXvXXMyZ6SYhCEM5+%I6{$v_7@?&JDA zUws7@81+`7Iv)(v?5MzHq0cX^<@d|e@if2>;s7rZdIPGN%i7PLoC{^w%{reZwRRP` z?~Ih@|5Dph5gtDtn7u}I{CPIHY3MY1)HOFqAw%VCbbavqPK!>6<-{c@Rkmz8nj=Qx zR!2{d%B%Oo!#eZ}5&IM(9Pn_2nVDIv{r@Q@q~!(}S5TgpqGD2coe`MTfTz#0NK zK>6$}ks8)Yx{izgiQ$ZL_PBsGW-g?hG*(Vh`BfiA<^FD(6LmN2xZWhagrm)IHBY(Uqq^`7Xr!&o| zHGDO9xHWE1tfA0pVLHUpP;;}uJn>q>)Xa>Dv9Y*>L}eFklD?`u2CWOZ*jV!qy~f?w zuuG%WeQ&;zqj7IL<0U)4ol!{;m&Fr&j{k{c#>+^_ngjkp${|bb!&ZhymY8U|-;R07F68%{wo*Na?q^&hejE6`>KXOy=3!W|MO%>)#Wz;{r4S3o#iVl@u+$JecqObiJ{LH>QP+jcvKd z;adEIwWC;HXDid3*$cRlvYd%!!szPZITx1|#$@Q$uE^O)9Kg?A$#=Z-Hl8Wx&}x&_ z8P)mqeZ3w2J6!kbPRGAyE+q72DFyi|8dIaC`mxIB4baE8^Or$s+A%*8CaaWzXg^1W@#A8yEy$( z)&8coFLJgxqlO{!yB}ohp8}72=(G3UG>qg=hy-TMf9BXWGX&i!g;=oWn>|XKt@SoP z+q52?@!B}<%P8XWL`4tGR5jT31`yUownj!(X1Ed* zdRs3X=<&8H50p!NdENg7sSXt7rDJi*6v@)DwlC>e3Hm03@J@lJb(J@&e($p!lH*XI zk6Uq}_e25Yv|tAfM=?Zh3vOW5!-{&G8ZAe>)GZh*_Q-oIRt|xJ0}2^e-R5zK>Z;!9XM5 z$F3e16}fNHR@{y=giJyKDznKaO>;D3cykanbWN;@4x9cniZIRl?A87kRhI|kciO6E z&wg(E`P~H{RaG6jZ!z&sk4OB7hawm7l9kKNzqcj{qWrC`Ld0rm^lNT4VxpKO?0pV- zR5JTOjJFE#wex%bm__dDn$dY7ul6n4-?)pqaUu})!r37vCn3#d;>$e1D2B%-;pq_U zOGDK`XeWZIQI&yRSOr5rR5^;+9as0`!m%FP<>Kq`KNkCDPJ8#-XunwlF^eO1PL7D3 z1U#OLHm%z^8Mvz%n1jGQZ3`M$wBWt`gkF1Gnnp!0PXT9ZYk19Rl~DNA#^}MBW1=KF z!HwtK^g^2Z-fGG!7k-$1F4m2C#doXInz`?T~YJ8uM z(+gO3U0ckj`KxYR`FB*hD}C0YO$;Y@^R%F&2ASxsnvu8NaKEM9loapnFqUKP#rY*)lS5qomX(qfw9 zvu!fZdb-%xX6jW}HTXxQemCfk+k{vjXZuS)lYG`#|3j7=8&duxN7a5n3w_<&v8S(Z zvgVsP0)g=G80Ax@hT@P5rji$uEt*CaH{$C{0>g0@bt`ev1ZC?ou{(-NF%*ywC29Qf ziVbW|@Atcljb8DX?PAwEqVvnJ0|gpiI?T^=0(W_HZ8g3eXeW=wVV#KuR-(;ANV<05 z=34K_L>75ugDh2C(fMZJ<)ZUjvRk4e-~iKBF+DPnT(qG1QpL9?h%E9UNUP!}??a!brk2mwN9veUdWCSq(D~8F8J# z&{haL4usX@z>iJCZ(qZp{SP)x&sMe^Gh!l)FRH&S)RCpK+w*v*fsQl7qiN`hgkY^cHy8nnV34dHPTaPOnvxujy!Oc~uC6 zUNG1_ktH@793aAB#x$7g!jq0(PJ4{Zwk@svGLN;#BJ#sex9|baX#l7h-q?_b^wqV&=KO;c=PE_R72Y>naqSQjoq+|7jzE z-#g=&t_lIB2=4FLQ!|0c-eo9$FZtxj&oH$9!NI{DTH4L#=zlgLT9D0Ti*ynLt4JjU zH*)0JpvOj{_EEIk7+i;~#zMysDd)i~M02gEY8WxF#&D3S=7O^LC{$tV`zNCDYO74~N0( zb(GjMFJxv)6q&YEm$GwbL9vuEiTAo?ExV3{B~zt_P6(g=aO}iDS41MoyOGJYavMCk zH!i0dF3_GdQ6X_Ew|TLakgCU|es*|JqXtZbg#x**6vR{@hT-i%Qq3CK z0!Lw4R`rOZ-cp2Kzjx3kvh~N|RM9AT!X1`+^SPE4qasHdT|EvSdLKAU_{*Wdbv)E&YyFX|9t~9RQ zR6{j#QqP`zgafSa@$G{;blh@hoiksnEAmcGgl@#CYinc=C%G?EHBSDN0)jXoZ? z0AM%FG0cH6fiZNz(e5QDW=%<~MY(evp+@h@r z3E!@?o3foD$gLb{-fPA|J8yxx*TXcug9cc$oQAJvnM8kjowTOJtyX}}Mqp_^XQ@uV z??@cd9X^uiVJm0EwjMmp`zh7N8BP3c2>x`RySw3S7nj-mq5WU`k+q^_Gs|WRh)q zS@>2r+wk!3(mv(BlXZREuovn)+uH#Fwv{dW|H6&2L=Bux(<*6GohjajuIgYF?p z^OyI*oF>)Mi$1s_?!b z?|}_^)zw_9AFjaR_RhUxT`RVNXNF#X;dE=4#`zj|o}X#wwkn@J)B-ZuveZ_4q9^ZU zc@&JS5Qx>4m5$=Xdw-J>A}~#?hE`aEZ9)tl%;xq|g;UL&I?`and4hfW}Rs zDzB*bt2Z*LC?`u;t8v+nnY^8QrG84I0AKUmyl+ii7^YvUEU!U_!a!rZt`t|#KVqw5$)GDs5u+1Y|| zd!eZ`1&PK9yZ=zQ-^UexlMfkmKl;CEEFZ2yIa+}vFy+X;N*}kEZXwd$@`u`AB^r<{ zaB_cQ`DFG3`R3M^gS~yo7al%7lSV&ZH#hv&bglay{ofp_tbX9~<%e`H(F_7M3&H@x zem`3FwFKzhy*t(S-oYtI=(`>5?8bszn&##{)Aj36>Khpt7#JE-n~hJ}#tX#@|K;73 zh4$#-`ir)383^934)Agv1n5kNei(ly68{K*M0}aBBw%4yLYiMv(%->hYE8`MCjtEU zWU1Ln>IS|U2*H~=`pVN(t)3=NQS@i@v$3v z5!?8a<<;TV^!UZ;dy^YinIi;IBOs1?&_VaT!? z&>k2C5GWus7#5y&HymnPK%e;_E(LvtX>chym1;14PA14=wfyaeiay<|_a+Eq_2rf{v zFg37K7BQU8oKr2jEOX{wM zgF6sDE|Id55;MIN2qCRo9`D&P64KOZn+cuJdvfW;9e{|V=?>Dsg!gtS49Q>MiedG3 z)S%!GH!^d70%7-$A3qKb+?V^_eyEmzv^Okwi(aGu*0fK>htIZ|VxX|Fu;XPD_P*28 zIqRg z+_&wm7lM-+L#*FIHZ3(r4N zb11(25DFj#cMH%n#)duForuiD(}g7wkP{z$6+HBj$#A@_7J%|DI_|l(zk7GNEU%Br ztH0wG&RL^iVga!b$cJ16YQF1sI{^o3K?n*z@)4 zKc{RI_Zv!c`Q1~>;rU))t0}(S1&mqz zTGywUsyq1B;wBFW5ZL5X0ek{2$IT%#2|#s41?*6eC*V9GHVZf~wPz2PB2%Assd`H| zVC0N#AV9e%e0+R~`Q3dQ#aQg&(Jm_)9q(HMCgWrsBr3d$J+9dunW?EoV7@Yd#oaO> zdY7~^q?-p57|WzKCYdZo|4^!~rT`_IUn@d$syL6ya3yagMS*R=J z1O)(e81$O}D1)ph0rLogSY=8C6X59FzlFdF$^Y~7k#Eq51^!g;T@MM*Rh&xgpMmMD z*^^To|HeO;5@38DJ&~EsvzA`epchY^hJi_c`%U)|K9Sct>%GNWd%wM<{?W_TP9*|X z{^DnT6xj29q}D*>fsi_I5rSBN4prwVJ3}OTokVLy9%e!yGx*u@tVA5jW(Y5u4BY&9 z!X1o3FFhMkl?-pArY|Vu3xMrlBXk5&X?3OA#v-<)fxs#K@9$?ppv!?>aOhubo$c~~ z{F!?$BlaByYKUUBzG!W{HQ&HP_*3mhd*Mzo(Jy0q(2Ji=;~(A%@c<(z^~S(;_fQh~ zR*+Dy0*^gr+5N}ewF01Tt|b>5-g+P7FWky@jaKgmf!px(&0HksAO<$mT47!^pA+6Uuqh9YKa z8I^Rvxe2nJVaMX*IYO71n*6?uX+gr)lq*T3E;U7mNF*vVD7x>@^oF|xBf%8~+#BaN zVAma=gFsHPWb1cCQ_?!zkro0B`cd8|a)4~C6pu|`og`A*T68747To1N3wE~L-M;mT zax<$)k1xn1|HDkq#7t%MN5u+ugyTQtTRs55yeDhN>Xj;x+8HhHaN^f`*Ff=ntNyK< zwpQ6_jb{l0DkE`5@yL9VTKnHiTy7=h3^W8ek|j3TWcu~#I>sgl$>JT5GoLf6j`odb zO~X4-4E;b1kPvyUH`Ty??C*CQ%z5$ytYB;Ad-o`>L*jQZa4pEX^9@ws_%}=uz$F|D zioX=}^DMQI4@`dH#d6howz5zIYgI=6g4@AVP!JWayYyqBhj+`2EmWb8d<_LR_R0uj zGdv-j2HF4SH}K|ym!mHos+lpXfA&YO#vPG)vo^P^$;6xpi`VjzCj)^zaTlSJquq*- zd8CWw(2p;I0W&3jX}jxr;Tn!k@es{zfgJFNMek)0s3ln*vM9Os_Vl`Fa%v`-8sP-V zDUR>;HGdThI-v`$xE|ez>KI4j??UX@wBErxlk@}(ln-z$D(ozZ6ZNVdJ*0qj=zm9z z$UN@zp}T0J$l>55Fs%EdcHhJ(D>$LrF0)+q{#HvTUrrbN3TKF7@i(V&jIzLJDZ&uSNBdyR8|YVOlSr?%jixUVBg_S z^xXf|BRy;DXH0HLTq@a(e@~M4LV(!CIHXz{7Ru>Ph1$pmIQoyx>yGb4SacoUQY}Oq z&%XN45p=6=8EC1}vML5$f4f92LGUucDg(x9ufqbWxrrC;i~xzMW}<;XfF=0%4ZRB$ zO8c<7B*0@&(QQ^+2d=9dRvBKawN4Cx_^K+E9M3NOS04bkg_AmxLJsk=+-`im>#F%^ zY4em=AGK?LnMdgFDnV%pk2>gI3PJy0DS8TrTpay*dj^;+!o&BloyBVZVFp^#fA9Re z|GvlH8v?)w-)mimiwqiBO2V~-zjxl`fwM;c?Kj^3y^!#~?OEOw<#X|lvX%8exAKgB qpC0@k3&qI63C^1E?E35vTwIqB7)ZX4yz`q-q9Cg(Q!M==;C}!jh3h^5 literal 16305 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 literal 23041 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 Ib=1>;0Mic4`~Uy| diff --git a/doc-src/schematics/proxy-modes-transparent-wrong.png b/doc-src/schematics/proxy-modes-transparent-wrong.png index 6bac491f14216dce86efbfc110f2fd7b74f55663..ca501e934674f09a932bdfc0a490466af5d28f90 100644 GIT binary patch literal 14719 zcmd6OXIN8Pw{8#=BOq)ds31*BR0I?hq=c@32th!45vhWJiV#RB3Q+_UDN;fSRjL%J z(gg)6p(CB3NC~|KLQ8U2aDV%J=RWuRxaB<0{Q+57YtFgGeCL>>ymKT(PgjHG1lI`= z2*jeLsjd$KF|dF@bg<(`fIF*}%*DV5ork`LDyXpQ+&u7w!BIs=1qAx^j_JVWDDWNp zSku%41Y&K*z$3<=& zee)K=9oR^Bhxz0<$x$%{v+Jw4iIJ7smi;a@r@)82_-W49p(`oKZ+3GdNrDvy2nPbi z&4c^RE7?_#3!vFlUyC2lf}Zms2>khay!Yq}tn%TL<60R^M_@154j)9_I?aGQLhEo` zKhOdSxqwRZ8xBpgJ-CA0(;=vEcl4pDJ zT}@^})g0ltF5|H4sIy?uTWa2Zm{4aZ~2$LnwA6S3cNCRXV zsvhot=loykH5Z6Zh%;ye5 zuxuw`jk;CXwcl&WS)-pXTg(-401Htv_;=3}kWRzC+*{W+TXo&ijQ;XohII0YTL~(-?6dQ}hz5O3o4G^g9JILE7~%02sW`18*FT@k?OyU>s64wg zt-$BUg{o^+4|icIPu#rMw^IjA6Ak10g82yaJag>KV;I#A~z9 zgA+zU(VjEmI`!d08EGu@Hfa@+=+Y~T)b0nl9wXnD5e5q1zR@Bwm^rnFI zDZT+5h|2kw>gyQ7dJwR-4`ZCfqc9(CdW3sNrDaKMqt&|a)?R#FmwaQe_InDuk9N9G z{Pnooq)hwt<>4e3@N#_J`qGE40uMyZG)rX#U%dHuu+FuD{0_Il6pf0{+7sTV@0kq= zRYJWHuleR2b9eI8NFln{TI$Y|@M{k&A8rMdOOoJ}!UdDx6N@GO0~4;C)G$YhEjQyZ zbM@l_M$_x6G$6&&9l3k$nz0|v`}waYexmR`GI|m_t&`YSrR^ZNLVQvO6Tm*YS@R1Q zdqkddxqb(^2fK1kt`*s=o_h#c1-tCg88XYR}|Aqi-)_nav3?Do4tDB zj|4`DT*#_bURj~mODvlhBGcT>^T%NIAl%Zc00(!##~4wJqs!_8nyAVTljIe+l9D(z zSH3RAj|Z&xeCQG{ufB<9dvH@p`4ARJH?_7Z67!9L5`mFlzZ5@I5$}C-=)oruyMn1Z z6+%dZHsXa^+b3o^4dZX)8h(jTru%fAn*RfdKXQy<*_JcQd&Zu&<%e@}PMDDJ7e4p3 zQ=Ov*UtvA8e!K9)qj9jQgVud77ii5`Z$TIWH{nfqb=tk*{x)ZI zYjrgzDLZpVd~0e#J7)0u#Sw^PWyjdl`Ft;F-sJ}QmiqK`(E>`5kponX>g!c2uV4my z;5x`-a3F!7Vl1B#JC9})-5>}6<~V{qWL7j(YBj`xw^tvr!k*;aES2_*3AyDx!3imH zQk=d$XNM$h%-Z46cZ5gHBpo*pmq)47=7`_O6=7U?4Yvg1Yx43UiyY*c$6cofj@Fjz z7TA$2SY$3*%9Asb>_jbS>pOZ%lW#}R@<*?#^71cBJIRo ztlevMs1?(bDeTujuZfrccCG5G7I}er(#6Bx9l}v%=(l*FHfQa*;%7g&QSr!ChBsaCp;~$Uyt1h@^!)AU+r!@c;QcEbYq|4LI%N=l>twa$Ztz z?2o6sfAA1_wRQqqSusqQhSSo$moyq;e9#JS;%xymX+gSPw*w3+QZk6KHFe3Vn8s z);aWl<3%s`i0H;rDd*r>e>XATr_-YDZPqMrmaO!C{M~m`BFtXZR_u>vr;B1C<>`&O%YhYHe z=d`KmeZ{6)=brNf6+v^{Pj@L|e3#3Q052zkXgI3!paQY$`Pm#*_R6p!CkMN4T<(6N zG-O#*X^;(+LJ0;Zm>dJ#Na!4eovD7Nby~k`=V&XEmIV_~v@DL;X4nd3sNv8wyjpDZ zZ(S>(nS7~P*_4&&@H^u#0jvphG{9eSkr@1LH{K!F88ALcie~SI1=i_7J}MdiQQV^gk51*q1c>Y7Q@h=rBBb5!^qdxIS3m zvoc<1zv2W^mIY@1TwrtFxCOgz_Tsc9yHP`x;~*V~cmY88i_-|?0-yp)z$$|>K121` zL8(Q6Vu5a{z*B`l$_)U}pf|NxWi8NiMRi~|pJ_b2rW{_69A4K0iViwsb=Y2sw$+(y z4#(-HnTcC{wDD<+X?RV3gCIyB5ZRW2I*ohY(g%AH1p=|r*6P9ncoOM69k{FLdeXJ% z#Odvma>fHTeGtkFx#5ElkTe0Sb*X)uVTSnpvag-fH{UT{!JrY*1^#*PP+SopGbj+y zL5`QGrl#=hOeph8{PoF7wX#m}ZFEh(wE^&)i#Ecg1@Ou)dS_R&ap+2p$5H>H*Sfcv zv9U$sOFU)9?+G)ZHKy)@y$<#=16k?XW4@vEpdKiIi{9O0=o?m91D~{D(`au@V@G`E zrBfudCb0lfke)ga`KfO@oPJtCD1SgA9LhV)6YGQwBiC_- zeEzm{pj0^;7}JW&8jknJEzU3{pLm_3lgU_=3-zYIHfPVA)_DY^ca|pa(=KR^EHV{J zxH_V=paF;X1?*dE!;jxPMcdV>b%&x6d^9Knwb3wvVpArdPY)*z&(>A&dRY9j@W@5~ zOjx&k`=h~Iz!H@m{R1P~$qSowp@+y$+STm6tmMy8GI5rf3gloMXz4{@JAy_7u|2x^ z`T3iXBO@d4m~?b><|8ZJCL~4aokI2C$LsV|IyVZ0@s;adQ?hm9AohHy$=Er*Q0>nN^kIhb(+N;6W_Q)TJG)v*U5?2uPjJ59K1mq;I!i8iTd;4Y!X21bG`wN!Fb@5n6 zM#gO7$;9MjKhgxEQwDgTe7YBsC+od-Unb?Rfth@I;MO&y>h%UHd9~Bn@~WbuKjpw@ z6UgX6Jz_+(b2|w=E!?Hj(b3^xk(RZE796W^!(6dO7!DQY1c|?cDPECwtq1&H&RJlP z8B|kWUzES))4?D4#+|7~2|pkoLsp6P%afF%uNvt^w$bJX!uEIEz&cqjBao(=gw=(@ z7qgW-z`ODZ;2&$X8NPiiypxVxdIOLO|1pdVpdr%-y>;O*84HP) zJodHV*g)lr#E%l&+9{#Lm1fx*VV4^!nfw0=Ji_NnZ}`_DS%(QEz!FWL zO7bVmh)RCBQ@*u<`ydMwkTOLW8wPa9?Ax-2Z$l^G?-IUeL(c_rUk2c=?476wd^7vs z4!%1BY8Xvqz_c$4C#Q}D`u5Vp1jrpog)a)+-$P+Pbi0+*+er-hXVm*J)%}nIoA9Pc zWY7#!wafKc1z<1bLjH~#<^X+4gHE?EgZHZBCh^ke6jLHXvwn_^H<031!FY!scl`Wd z0=11#VFGi~eUR$V|J?6sR;?SHP0}5oa0-MGuYot*jk-78m(+%6$37kt6-m6u^qXUO zX1)d5V%GILh@076=W~rStG4%;gt(ZA#NY7=Rs){%3L61;=CL$qqwMsGQi~)g#OdZ^_-BC+gx3(E~5}& zTXeOd!xt*C#kS9M-JS+-^urABKWJgoAFx0=0;T5e&SPIAeGZ05MqQWBEvt)5=+sf? z^6}UE{;gTBJIrRjtDugr!BFvQHe;Qz&+g-G5Ma~x*Z`eG9_cwnMAvGneg3?&e%d;j zN|K@yEKkuK{T}3>=l3&(G}U?#y<753_IXGCd8VqkC7v>06DZvh*~7^5u^H{Zcn{x{ zBTWD|T397WBJ|Yr0%iEQ0lP+o0$ZDi_U(i8ff0KG^p}<~uZyBF-?Mw&0|BQ%=w}_= zIHn$L-sk+yJb4fd6Non#l@S;Bzf6ruGQDvL@%}##V+7aZyJ{5X7V-MF>e|HJA(WyD z7htTgx3|%q`(0FWN>*tkrV_RURhz4&lk0BqYUJI{u#Cn(7lKo^4N!J%i(;4!m`e4F z17pB8jZM*FLZv@CxQSHV5oE&o979PiGaJFJLMb@`c8Oeq(K|Rs0rSNK;R2;`9ard_ zg-RbJsVQQl1kXnvzm#pPNuLHCFlfVG+1(W@4eT)Y`%Xxz`r>2x@q>HHcQ@mb1hHF;N7UqA}wkxqB_6062_bk`qlP8$wL(nU4Yjt%VBU{W z_V;3a*XD##!|+#KZK@$>PxWd`ugbf>yX;dZ3}g+UGM>W;)z@O#x-%yl`t#?#=do-z z1R^Z;)hir2XLKV%L7-HsD~GgV87J3TGW- zQJL#Wp?YrBBW25(H;vx69s{!Eo)o}7KC1#~^3^paH8|2By;A9H-&b}?*r@DYlFS|G z4ezxA9sHGKQr32&%BR~wy6-S>&bbEJStUyiZuu6}&CT2$Og z1)F2db|Eq%S|1wi)5n@*NXGVE%Q~(>n5C_5c!Vhh^D@IqE51tc;-|P;C0G(q7A+?R z@`0Qz5!kPtn{%V2I|o5BU%Cbkz*6Fp*C^@0HS$C{^-{fCb`G}X>1Qc5UC)1ar&K%D3lg4NTB zLp2Te3Q`YRx-057pRr>K?NhqN>{Q)Z`$uH=#t-_0rJ6SI1OD!rb`wGSWKUeOW1WYK z@o*8Y%d4p-UsO_%_JX3TWM5wk=3H3YJ>g@3rx8r7Q;7b4G=VX{tX*N?j2Rh7({KYe-w-D`|16MBi+Y_JyJ??}mR()K! z>r4)CT_|WU?u8soUuP)LhwsdcNl98cZq{$&KWG6aky#Ww2>mLAzv*?`%Dn?H@$T^D zD5?ldCBQrhzYK26h~4dfwfXo{7B*rtzI1&KvaoyqhK-^LL|Nz0%S4yJs$_dnn0~v8 zc6fW2&5Z^2?K84y6K*YsHjRp0Ouk~_*rvnT)NtbVU8o~d=*Wv_>`9d)7I+=K?mjK| za6e0Y_`GN+=fZmLg8>au)KRF&roVd3q}dRW?L0jFjNkym>cyY^$qI^1$R z9~JnXmgu4AOXf$g5ftxy>q=MKmF%h-SlaYz&3=4z1l>j}r?+<-wfXXQwtjj!`NOW~;P!vE+6&bS0TN)$K64*=YMqST-pqBP^iU@VYWE&MW^q~x&-o*Q)Cly;je8$_|cB~?zkTEQ>$vjMrsl|G2k>SVO0 zT14w<{PRahfZd2T+_Ek5@3qs~$UZ^p({EU%*y=Xzf6xtx^a!|G`VQ1Pw9;ZuIneZyJC|D6dQRPTVby$AFRt_`sd{Q%OnUJ+o*^wR*e&e1 zMWozkvuO6x*gB~5@4-m(VxOl zb#x)8&cbU0I+c3p z^`MLm(`@M{(UeAgp0@)a&!pkJE_G-~IG=M2!F=wgVF9$&M$27F@#N9UpL{vE#>)Yv zXJutn?lKxKik#^RH#Ih#I_|n$P);4Zr@dsk7eNn5lLy}o)TqL;U!&~+>)wd4&6n7{E7QQWUF3VP#$jzXPnz(RNGHfR9+CuEb|nb~IWJRZVoyqzX01UnV83N9hrWqyn~m$XqCG4bPzLwU%jB^w-G z^}MOJ`1Sl?FI;uHQLq=vVDKqQxqT^nGz*an!&9;&XPQxcOBvn`TWE7p1Rh3pPi`F% z{p|(!p8BzD1uOPgI#AhUWs4jw_Sr#gKX0s*u0s-E0*=-Fz#yiN&wb9=DsnQgpiNL~ zO-6ieUwr&~5_+p<_G3IL409PFv!cXln&`T9Kg#FI<(l+z)p-ei?*qjWaz=OEv!7rL$(@&i zUk{_u{}v5SDX)2Py2xv;P6rLV3a?W&*H>@5g12 z!O+Qlr#XU$6-@IyM-y|Xmd9F)rj^{Z|FLkaDxBsT6u*V$Gd$CN$GL+u} zq?WFQzWy-dvZ}Kju>39DbyXqRH(@Ma!+K4zae1dXGLu&*1f?oBXXJj6qR8obP@~|; ztGh*ie;ktDt!C1Zq6>s1tfjS57oV#hCQ(PzGtBUYR$J(#lX{OMKZLt`oJ^(5l(w7H zYFh;_7<`eg=N8)F(*;u5#@%!D$RmGus~Wgl@$;Se;xZWW^|~CI-YFJX?V{vin}(bE zIZ!keXVWD$y!p_v7|9UEyBGe*z}Aj;)&It)nnKgQsUluyL0Q&pE%@UTY7V7H$ANmE zxm%epdLYu{L!|nv)^uC94mpnwiZA8)))vD_2d7|;@J^TVy-vT)dz+mSmU2>vr2!mD z7k{o%xLW z?v<|7_x0X>dZEp7};(uBxEBI?@|GZ#)y2JDbRA6Oc zlx37C@v(C}GQ$5a{Kq;eB6>9KkAy-n8!Cm2l%53Zt4=pOz4>+l-21D2pUD&nGDpFG za^Reb#zZF%_SQb0%hc)sl9(_&48#8s%g<`mHYsSx@_244l&onl5m%%J6PVC!vJ$<1 zl`|oCpEbL!P=ve9-=-9CYgrbSW<&rxod9T%}1# zY1#h;+v5=Ty~2*7dko}iLqgZ9!-oON;o4CkuHS&!`1||YH~WeZHa0c@hMQt$QHQ)2 z8}ELC#(&x3WYOcB7?`VS0A31aBiI9>QCQ9Z_d{dNTYG!&?jk)sJtyn^HkQZ7lYHgn z-KrE66f8+s{J>A007mM~ z(Ac`!ocU0*=*lE;G&`H|XSG@n{E^k#jyYQ)uO-WYBV3xqtOb`Zk5@g@db--eQknxC z4*s|-CRVrJ!mZC?$L`MQ*)laXRkK>(5giT!5v~%^icdDD+qw}e3Y8E0BZGN2&NH3% zTCMk6Njht0OhmWG#>Ktkg;hItW0ffD?*#uM>t^Zm&I$@!XIv(Fr&V5&le=U-))3I( zPZ#75qX(r*?d^Hubb6zUDzl)(3Y3hSCgWPDidE*7fhyosbk*B?)=&&iS+Hq{P@ACD@W2{1B~U7T#%$TND` zVf(J=gQdw}!^^Eb!YTor3|Lhygt(;~dDzy9?m`NtQKd9Zj&@SEXJ7+oTj<`M*SMV6 zeZK-=rx9Qj?ptx)UyG&DKdu?4loTtC=p4&|oZdbglq7b+fwAwb2i~ESvPt2yGN4XS zg52@D@N#~+7x(Lprqdg0`229)WQ70yEi}7m9-Rk?x)$U}zVzv=XcMjw5sR|~5 znS_qrktr0{$y4p(OqWa*4XsIUFx@OinDry(nJuz zG(so%&l~J6sfsl~h33u;aQH~b>OoIT^`H7B_IZfoo%<)#FkDrDpLIa4!@~N|e62$A zdq<>d2gmQ1NXs9LA1~HFcA)l}9_hb26_!-coPBD^hKNR@u|0jbpQ$pdMG4i(`n1EV zJ%fBK8#NNI@ay~SYl6o4>XWhF_`7(t6|DT?sHP3PI1K&3S#24qs!+u${-&y2#nSSA zvE0!}MaGLOVC{|}T-2)!C@~LEKJRYDsM*JWg2%i(3RA8(QFxZ~^X%<8Y1ckM7utCv z0WO9NOh{f-vJKYd$sDkl#0`lO8C(j=li*Hz@W){K~o$aw7^fVGQB^0Up?4&@| zHOxBWhs`a3>8(h4^S<1S$l%BrWy$i$DDljunGR;2l7Ld(I3>6%=i?Xn1Cn&L*M$SO z9UXnFE%f%YSB-KQ{&8&T<`P~bF#B}YEF35C66(nF6ya&_bkGcV;n{t50vIMC`ywLy z=d!`(X!EN>AfqQCID_ZUY9YrLWHncAE});AMs+Ryez~roSP3=Z_Vt|{jwL?42`r!d z{bO@Pv{NR^tuf(qWztGV6QLkDeZ&{TkpVd}e`&pmAgWft{Z31lXSiEK-Ut zAA7;SjM`ml+|vrsggdD}MFz@91nfr(?CZ5{lvPL{7tqSFN*;h2upis zF)Y7x`W-X#n?!d$T)4vCKj6`v1roUCd){Pq>EVs7K_UZf*8x%F zgIfdiu(UTXdEhLXkhP71#{RtrmC*RcwhtgsYCk|`mCh}CVMoZtGKfj3byobDWFXkS z=V$!E_+9-^*XfbEcdcFVVQK_$E2={fznb$zmz7|&*7`Htv{W3BO($!L--*sXH zO^&LX>IcWF2w=aJ&1CsHWCXe!?!@Ww^k;9~%_8$v^{Z=dm`~DxzMv*TFxZ3wWcY<> z)ap!E0y%W<5iWHY2z=}4^rgJQ6|=4R*8wK4PCNQwX^ZdJWJ3(Lr~3qcUbAWLs!aQi z-qXHRt21Tp)C)C)AZovPR9xrRNo}j~J8&ntc`%>#UUH_Lxd|5^$?QcOGL}IFexV18WlOnQoGZ2{ z!e;3Vz*L~aoB%k}1>+LMUh*$nF4#CFy)x;)7=1`;sDeAf^WeT!+Efu~5! zVz{JP^q7{e(zZ!~B2xA>R!@ki|KfQpEbV8^ciK2-`XHgeY?)1p)QY)phG8H3EIqMm z9A^!+o_gd6hu8oVww7OhJ%Sw74AWa#GXw^NxyXLU1oE&Ib z!c;#5bl;Yt8=5&|(>VtTev(0YYh}=L@&SGieHnN|~En>iNmUp!+`uU)}@5slvuCi~~oywTcv9B@7o-*Mtc9#mm>3~sID6cn9Cf|gWJt-#Vfv);(y{1w8jNLo78ymm0;oib3 zl2SLED(<0(=%?vW{*g~9ZwB(9#~U6Ad61vN8b|099a%u58p-#Ht%4(P8~4r@$7)A8 zLI7F|SY$Ymwi8Q-0=xkhVx+knm;Q92AL1|h%x$kj9;IjzL5^HHlbl$C-RD{aD6AhF zNmVwH-9>z)W5esS}b_VJ-k4; z{#Y_CV3_Zdg_a+(DR(3h;3T8!rrRd{k+!?|cV0T9OrC)-f&H?i7?ccf#@)!BB`Ebs z>cU`QW9g9Mm7oWbRo&bQm4t@pISW5)p%?8vrV38iMe$bwk?-Lm00kq}*@`d~uNL$3 zn1chO$E5J&hF^8zBdC_UQ}~6AsZOOaL1FyE;BR}3lhso0;$Ce(B^u<0LK@>vbSn`w z<8}`A*5!w0PqXrDJY9X|5t!Zu>{SqG!XO2^Hqx}QS!7upci=QM4U}gb9t5YITn$UO z;BRd>!ItSA->XBb_W=>GK&e~G3MQe3vbEaMFkHJ@>q}nzTH1H6*73Y>8dPWlsVZ@u z;Vqz`pdQ%PJUA+aIT^J{ET5NHwb)%)VGOW}YWm5dsS4M$L#eL*)3SU4JfjBJ9EPPC z(`a7Wn|P6`{qxu!;7shW*7F%)Xy#`*q3tqgAQo=LukQ7x%f~oV4Q>+DtQJh$iH9x&m2KxHcJMMZ}hBXC(A zjD4Q<_uFA~0J}vSi}#QW0IqC2hD=?lUxd}oW#mV6;Jn5)4g>}4i^?^Mi)@<2(?hxyfTxac`e(Z&C(du|XJ+ccJ%{$?OMD#0KCI_0WO` zh`)Luu|^}2Yy9fsIA=#fBCr>14tH;rUEgI7dziaXOYnfAJ8$e1 zlH>Yi>`da@ZxZq0A|y5X^-}?M?q!9AbZrj5%a`&}5jQN;5iP#l2_y3Km(rluXoW2x ze&cIY-MJA}czJ;c8`%#=+4pP1>OD+6FQt6LmIzw)i!CvJKk>9yh${>AvOIEW>(*GA zvBW!U&y_<2EJkv@)}yYI&{M)AbgG;pLmXUe*f>V4jvdzT+O?m?ukU!Yo&tw0aF^I8 zFX8(jT!*km ztt@D~<8r?6Z?(M<#e0>x4L_tjOJ@kOH@7+LE7|TEc(}xq3Z{zABwuS)sA~O4fSKo> z|DQ&{mm;4L4{8>q``y3m!1NNG%U_!Gc7$!=k6Gvc9A_vcm)G$)0~GQfAgeoy=q&$A zt~Z%%W4Ok8xNlaZE1(t^kn)Da%ZJmLaCL&e#!8fJSE}7KINYTF{RQ*@sf1iK`sder zjYPB{D-8IN&|zW!|KShtXubbm{=%>EV09YNp#6_fIoM+4=4{>K5crRnc3a!ajI6u| Z0Rz~s07W&uXke8=S~qpo3svt2{tque0VV(d literal 17568 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%0ASaEk3VYw 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 F{2xqrp)~*i delta 84395 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 O_*x<$#Ht Date: Thu, 23 Oct 2014 16:15:38 +0200 Subject: [PATCH 22/89] fix typo --- doc-src/modes.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc-src/modes.html b/doc-src/modes.html index 77bd1b056..ddbeae0c3 100644 --- a/doc-src/modes.html +++ b/doc-src/modes.html @@ -198,7 +198,7 @@ This may work for the first request, but the HTML remains unchanged: As soon as

If you want to add mitmproxy in front of a different proxy appliance, you can use mitmproxy's upstream mode. -In upstream mode, all requests are unconditionally transferred to an upstream proxy or your choice. +In upstream mode, all requests are unconditionally transferred to an upstream proxy of your choice.

From 1ef74cf294dd0fc1d2555e5256e1b1d39ca5fec5 Mon Sep 17 00:00:00 2001 From: Wade 524 Date: Fri, 24 Oct 2014 15:54:51 -0700 Subject: [PATCH 23/89] Fixing issue #368. --- libmproxy/flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index e5fdf4248..13895a050 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -219,9 +219,10 @@ class ServerPlaybackState: queriesArray = urlparse.parse_qsl(query) filtered = [] + ignore_params = self.ignore_params or [] for p in queriesArray: - if p[0] not in self.ignore_params: - filtered.append(p) + if p[0] not in ignore_params: + filtered.append(p) key = [ str(r.host), From efd6fdb0e24532de757fc90a8d3ae984b7170c51 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 26 Oct 2014 17:13:25 +1300 Subject: [PATCH 24/89] Start a fuzzing architecture for mitmproxy --- libmproxy/dump.py | 11 +++++++---- libmproxy/proxy/server.py | 2 +- test/fuzzing/.env | 6 ++++++ test/fuzzing/README | 14 ++++++++++++++ test/fuzzing/client_patterns | 4 ++++ test/fuzzing/go_proxy | 15 +++++++++++---- test/fuzzing/reverse_patterns | 9 +++++++++ test/fuzzing/straight_stream | 4 ++++ test/fuzzing/straight_stream_patterns | 5 +++++ 9 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 test/fuzzing/.env create mode 100644 test/fuzzing/README create mode 100644 test/fuzzing/client_patterns create mode 100644 test/fuzzing/reverse_patterns create mode 100644 test/fuzzing/straight_stream create mode 100644 test/fuzzing/straight_stream_patterns diff --git a/libmproxy/dump.py b/libmproxy/dump.py index ccb2b5b54..9fb0f0017 100644 --- a/libmproxy/dump.py +++ b/libmproxy/dump.py @@ -1,10 +1,13 @@ from __future__ import absolute_import -import sys, os +import sys +import os import netlib.utils from . import flow, filt, utils from .protocol import http -class DumpError(Exception): pass + +class DumpError(Exception): + pass class Options(object): @@ -37,6 +40,7 @@ class Options(object): "replay_ignore_content", "replay_ignore_params", ] + def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -71,7 +75,7 @@ class DumpMaster(flow.FlowMaster): self.anticache = options.anticache self.anticomp = options.anticomp self.showhost = options.showhost - self.replay_ignore_params = options.replay_ignore_params + self.replay_ignore_params = options.replay_ignore_params self.replay_ignore_content = options.replay_ignore_content self.refresh_server_playback = options.refresh_server_playback @@ -88,7 +92,6 @@ class DumpMaster(flow.FlowMaster): if options.stickyauth: self.set_stickyauth(options.stickyauth) - if options.wfile: path = os.path.expanduser(options.wfile) try: diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index fdf6405ae..613662c36 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -275,4 +275,4 @@ class ConnectionHandler: # make dang sure it doesn't happen. except: # pragma: no cover import traceback - self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error") \ No newline at end of file + self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error") diff --git a/test/fuzzing/.env b/test/fuzzing/.env new file mode 100644 index 000000000..e2cf7829e --- /dev/null +++ b/test/fuzzing/.env @@ -0,0 +1,6 @@ + +MITMDUMP=../../mitmdump +PATHOD=../../../pathod/pathod +PATHOC=../../../pathod/pathoc +FUZZ_SETTINGS=-remTt 1 -n 0 -I 200,400,405,502 + diff --git a/test/fuzzing/README b/test/fuzzing/README new file mode 100644 index 000000000..2760506fc --- /dev/null +++ b/test/fuzzing/README @@ -0,0 +1,14 @@ + +A fuzzing architecture for mitmproxy +==================================== + +Quick start: + + honcho -f ./straight_stream start + + +Notes: + + - Processes are managed using honcho (pip install honcho) + - Paths and common settings live in .env + diff --git a/test/fuzzing/client_patterns b/test/fuzzing/client_patterns new file mode 100644 index 000000000..83457b6f3 --- /dev/null +++ b/test/fuzzing/client_patterns @@ -0,0 +1,4 @@ +get:'http://localhost:9999/p/200':ir,"\n" +get:'http://localhost:9999/p/200':ir,"\0" +get:'http://localhost:9999/p/200':ir,@5 +get:'http://localhost:9999/p/200':dr diff --git a/test/fuzzing/go_proxy b/test/fuzzing/go_proxy index c9b6aef68..ea29400f7 100755 --- a/test/fuzzing/go_proxy +++ b/test/fuzzing/go_proxy @@ -3,20 +3,27 @@ # mitmproxy/mitmdump is running on port 8080 in straight proxy mode. # pathod is running on port 9999 -BASE_HTTP="/Users/aldo/git/public/pathod/pathoc -Tt 1 -eo -I 200,400,405,502 -p 8080 localhost " +BASE="../../../" +BASE_HTTP=$BASE"/pathod/pathoc -Tt 1 -e -I 200,400,405,502 -p 8080 localhost " +BASE_HTTPS=$BASE"/pathod/pathoc -sc localhost:9999 -Tt 1 -eo -I 200,400,404,405,502,800 -p 8080 localhost " + #$BASE_HTTP -n 10000 "get:'http://localhost:9999':ir,@1" #$BASE_HTTP -n 100 "get:'http://localhost:9999':dr" -#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@300.0 +#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200':ir,@300" + +#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@1'" +#$BASE_HTTP -n 100 "get:'http://localhost:9999/p/200:dr'" +#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@100'" # Assuming: # mitmproxy/mitmdump is running on port 8080 in straight proxy mode. # pathod with SSL enabled is running on port 9999 -BASE_HTTPS="/Users/aldo/git/public/pathod/pathoc -sc localhost:9999 -Tt 1 -eo -I 200,400,404,405,502,800 -p 8080 localhost " -$BASE_HTTPS -en 10000 "get:'/p/200:b@10:ir,@1'" +#$BASE_HTTPS -en 10000 "get:'/p/200:b@100:ir,@1'" #$BASE_HTTPS -en 10000 "get:'/p/200:ir,@1'" #$BASE_HTTPS -n 100 "get:'/p/200:dr'" #$BASE_HTTPS -n 10000 "get:'/p/200:ir,@3000'" #$BASE_HTTPS -n 10000 "get:'/p/200:ir,\"\\n\"'" + diff --git a/test/fuzzing/reverse_patterns b/test/fuzzing/reverse_patterns new file mode 100644 index 000000000..8d1d76a20 --- /dev/null +++ b/test/fuzzing/reverse_patterns @@ -0,0 +1,9 @@ +get:'/p/200':b@10:ir,"\n" +get:'/p/200':b@10:ir,"\r\n" +get:'/p/200':b@10:ir,"\0" +get:'/p/200':b@10:ir,@5 +get:'/p/200':b@10:dr + +get:'/p/200:b@10:ir,@1' +get:'/p/200:b@10:dr' +get:'/p/200:b@10:ir,@100' diff --git a/test/fuzzing/straight_stream b/test/fuzzing/straight_stream new file mode 100644 index 000000000..64feae450 --- /dev/null +++ b/test/fuzzing/straight_stream @@ -0,0 +1,4 @@ + +mitmdump: $MITMDUMP -q --stream 1 +pathod: $PATHOD -q +pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns \ No newline at end of file diff --git a/test/fuzzing/straight_stream_patterns b/test/fuzzing/straight_stream_patterns new file mode 100644 index 000000000..f5ae06f2f --- /dev/null +++ b/test/fuzzing/straight_stream_patterns @@ -0,0 +1,5 @@ +get:'http://localhost:9999/p/':s'200:b"foo"':ir,'\n' +get:'http://localhost:9999/p/':s'200:b"foo"':ir,'a' +get:'http://localhost:9999/p/':s'200:b"foo"':ir,'9' +get:'http://localhost:9999/p/':s'200:b"foo"':ir,':' +get:'http://localhost:9999/p/':s'200:b"foo"':ir,'"' From 7aee9a7c311e755147b398b8ba0b44aaec40eaf7 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 26 Oct 2014 17:44:49 +1300 Subject: [PATCH 25/89] Spacing and legibility --- libmproxy/flow.py | 9 ++-- libmproxy/protocol/http.py | 90 ++++++++++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 13895a050..6a24cc63a 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -466,7 +466,7 @@ class FlowMaster(controller.Master): self.refresh_server_playback = False self.replacehooks = ReplaceHooks() self.setheaders = SetHeaders() - self.replay_ignore_params = False + self.replay_ignore_params = False self.replay_ignore_content = None @@ -719,7 +719,11 @@ class FlowMaster(controller.Master): if f.live: app = self.apps.get(f.request) if app: - err = app.serve(f, f.client_conn.wfile, **{"mitmproxy.master": self}) + err = app.serve( + f, + f.client_conn.wfile, + **{"mitmproxy.master": self} + ) if err: self.add_event("Error in wsgi app. %s"%err, "error") f.reply(protocol.KILL) @@ -769,7 +773,6 @@ class FlowMaster(controller.Master): self.stream = None - class FlowWriter: def __init__(self, fo): self.fo = fo diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index adb743a21..9542cc817 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -313,25 +313,37 @@ class HTTPRequest(HTTPMessage): request_line_parts = http.parse_init(request_line) if not request_line_parts: - raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line)) + raise http.HttpError( + 400, + "Bad HTTP request line: %s" % repr(request_line) + ) method, path, httpversion = request_line_parts if path == '*' or path.startswith("/"): form_in = "relative" if not netlib.utils.isascii(path): - raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line)) + raise http.HttpError( + 400, + "Bad HTTP request line: %s" % repr(request_line) + ) elif method.upper() == 'CONNECT': form_in = "authority" r = http.parse_init_connect(request_line) if not r: - raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line)) + raise http.HttpError( + 400, + "Bad HTTP request line: %s" % repr(request_line) + ) host, port, _ = r path = None else: form_in = "absolute" r = http.parse_init_proxy(request_line) if not r: - raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line)) + raise http.HttpError( + 400, + "Bad HTTP request line: %s" % repr(request_line) + ) _, scheme, host, port, path, _ = r headers = http.read_headers(rfile) @@ -343,23 +355,39 @@ class HTTPRequest(HTTPMessage): method, None, True) timestamp_end = utils.timestamp() - return HTTPRequest(form_in, method, scheme, host, port, path, httpversion, headers, - content, timestamp_start, timestamp_end) + return HTTPRequest( + form_in, + method, + scheme, + host, + port, + path, + httpversion, + headers, + content, + timestamp_start, + timestamp_end + ) def _assemble_first_line(self, form=None): form = form or self.form_out if form == "relative": path = self.path if self.method != "OPTIONS" else "*" - request_line = '%s %s HTTP/%s.%s' % \ - (self.method, path, self.httpversion[0], self.httpversion[1]) + request_line = '%s %s HTTP/%s.%s' % ( + self.method, path, self.httpversion[0], self.httpversion[1] + ) elif form == "authority": - request_line = '%s %s:%s HTTP/%s.%s' % (self.method, self.host, self.port, - self.httpversion[0], self.httpversion[1]) + request_line = '%s %s:%s HTTP/%s.%s' % ( + self.method, self.host, self.port, self.httpversion[0], + self.httpversion[1] + ) elif form == "absolute": - request_line = '%s %s://%s:%s%s HTTP/%s.%s' % \ - (self.method, self.scheme, self.host, self.port, self.path, - self.httpversion[0], self.httpversion[1]) + request_line = '%s %s://%s:%s%s HTTP/%s.%s' % ( + self.method, self.scheme, self.host, + self.port, self.path, self.httpversion[0], + self.httpversion[1] + ) else: raise http.HttpError(400, "Invalid request form") return request_line @@ -371,7 +399,8 @@ class HTTPRequest(HTTPMessage): 'Connection', 'Transfer-Encoding']: del headers[k] - if headers["Upgrade"] == ["h2c"]: # Suppress HTTP2 https://http2.github.io/http2-spec/index.html#discover-http + if headers["Upgrade"] == ["h2c"]: + # Suppress HTTP2 https://http2.github.io/http2-spec/index.html#discover-http del headers["Upgrade"] if not 'host' in headers and self.scheme and self.host and self.port: headers["Host"] = [utils.hostport(self.scheme, @@ -380,13 +409,16 @@ class HTTPRequest(HTTPMessage): if self.content: headers["Content-Length"] = [str(len(self.content))] - elif 'Transfer-Encoding' in self.headers: # content-length for e.g. chuncked transfer-encoding with no content + elif 'Transfer-Encoding' in self.headers: + # content-length for e.g. chuncked transfer-encoding with no content headers["Content-Length"] = ["0"] return str(headers) def _assemble_head(self, form=None): - return "%s\r\n%s\r\n" % (self._assemble_first_line(form), self._assemble_headers()) + return "%s\r\n%s\r\n" % ( + self._assemble_first_line(form), self._assemble_headers() + ) def assemble(self, form=None): """ @@ -396,7 +428,10 @@ class HTTPRequest(HTTPMessage): Raises an Exception if the request cannot be assembled. """ if self.content == CONTENT_MISSING: - raise proxy.ProxyError(502, "Cannot assemble flow with CONTENT_MISSING") + raise proxy.ProxyError( + 502, + "Cannot assemble flow with CONTENT_MISSING" + ) head = self._assemble_head(form) if self.content: return head + self.content @@ -937,16 +972,23 @@ class HTTPHandler(ProtocolHandler): try: self.c.server_conn.send(request_raw) # Only get the headers at first... - flow.response = HTTPResponse.from_stream(self.c.server_conn.rfile, flow.request.method, - body_size_limit=self.c.config.body_size_limit, - include_body=False) + flow.response = HTTPResponse.from_stream( + self.c.server_conn.rfile, flow.request.method, + body_size_limit=self.c.config.body_size_limit, + include_body=False + ) break except (tcp.NetLibDisconnect, http.HttpErrorConnClosed), v: - self.c.log("error in server communication: %s" % repr(v), level="debug") + self.c.log( + "error in server communication: %s" % repr(v), + level="debug" + ) if attempt == 0: - # In any case, we try to reconnect at least once. - # This is necessary because it might be possible that we already initiated an upstream connection - # after clientconnect that has already been expired, e.g consider the following event log: + # In any case, we try to reconnect at least once. This is + # necessary because it might be possible that we already + # initiated an upstream connection after clientconnect that + # has already been expired, e.g consider the following event + # log: # > clientconnect (transparent mode destination known) # > serverconnect # > read n% of large request From 16654ad6a4ba4f12287d5707dafe3794b6e33fb8 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 26 Oct 2014 17:58:36 +1300 Subject: [PATCH 26/89] Fix crash while streaming Found using fuzzing. Reproduction with pathoc, given "mitmproxy -s" and pathod running on 9999: get:'http://localhost:9999/p/':s'200:b\'foo\':h\'Content-Length\'=\'3\'':i58,'\x1a':r return flow.FlowMaster.run(self) File "/Users/aldo/mitmproxy/mitmproxy/libmproxy/controller.py", line 111, in run self.tick(self.masterq, 0.01) File "/Users/aldo/mitmproxy/mitmproxy/libmproxy/flow.py", line 613, in tick return controller.Master.tick(self, q, timeout) File "/Users/aldo/mitmproxy/mitmproxy/libmproxy/controller.py", line 101, in tick self.handle(*msg) File "/Users/aldo/mitmproxy/mitmproxy/libmproxy/controller.py", line 118, in handle m(obj) File "/Users/aldo/mitmproxy/mitmproxy/libmproxy/flow.py", line 738, in handle_responseheaders self.stream_large_bodies.run(f, False) File "/Users/aldo/mitmproxy/mitmproxy/libmproxy/flow.py", line 155, in run r.headers, is_request, flow.request.method, code File "/Users/aldo/mitmproxy/mitmproxy/netlib/http.py", line 401, in expected_http_body_size raise HttpError(400 if is_request else 502, "Invalid content-length header: %s" % headers["content-length"]) netlib.http.HttpError: Invalid content-length header: ['\x1a3'] --- libmproxy/cmdline.py | 27 +++++++++++++++--------- libmproxy/flow.py | 8 ++++++-- libmproxy/protocol/http.py | 40 ++++++++++++++++++++++++------------ test/fuzzing/straight_stream | 4 +++- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 83eab7eef..4a3b5a48d 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -248,14 +248,18 @@ def common_options(parser): "--stream", action="store", dest="stream_large_bodies", default=None, metavar="SIZE", - help="Stream data to the client if response body exceeds the given threshold. " - "If streamed, the body will not be stored in any way. Understands k/m/g suffixes, i.e. 3m for 3 megabytes." + help=""" + Stream data to the client if response body exceeds the given threshold. + If streamed, the body will not be stored in any way. Understands k/m/g + suffixes, i.e. 3m for 3 megabytes. + """ ) group = parser.add_argument_group("Proxy Options") - # We could make a mutually exclusive group out of -R, -U, -T, but we don't do that because - # - --upstream-server should be in that group as well, but it's already in a different group. - # - our own error messages are more helpful + # We could make a mutually exclusive group out of -R, -U, -T, but we don't + # do that because - --upstream-server should be in that group as well, but + # it's already in a different group. - our own error messages are more + # helpful group.add_argument( "-b", action="store", type=str, dest="addr", default='', @@ -265,11 +269,14 @@ def common_options(parser): "-I", "--ignore", action="append", type=str, dest="ignore_hosts", default=[], metavar="HOST", - help="Ignore host and forward all traffic without processing it. " - "In transparent mode, it is recommended to use an IP address (range), not the hostname. " - "In regular mode, only SSL traffic is ignored and the hostname should be used. " - "The supplied value is interpreted as a regular expression and matched on the ip or the hostname. " - "Can be passed multiple times. " + help=""" + Ignore host and forward all traffic without processing it. In + transparent mode, it is recommended to use an IP address (range), + not the hostname. In regular mode, only SSL traffic is ignored and + the hostname should be used. The supplied value is interpreted as a + regular expression and matched on the ip or the hostname. Can be + passed multiple times. + """ ) group.add_argument( "--tcp", diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 6a24cc63a..6136ec1c8 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -738,8 +738,12 @@ class FlowMaster(controller.Master): def handle_responseheaders(self, f): self.run_script_hook("responseheaders", f) - if self.stream_large_bodies: - self.stream_large_bodies.run(f, False) + try: + if self.stream_large_bodies: + self.stream_large_bodies.run(f, False) + except netlib.http.HttpError: + f.reply(protocol.KILL) + return f.reply() return f diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 9542cc817..e81c76406 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -18,6 +18,10 @@ HDR_FORM_URLENCODED = "application/x-www-form-urlencoded" CONTENT_MISSING = 0 +class KillSignal(Exception): + pass + + def get_line(fp): """ Get a line, possibly preceded by a blank. @@ -1001,19 +1005,21 @@ class HTTPHandler(ProtocolHandler): # call the appropriate script hook - this is an opportunity for an # inline script to set flow.stream = True - self.c.channel.ask("responseheaders", flow) - - # now get the rest of the request body, if body still needs to be read - # but not streaming this response - if flow.response.stream: - flow.response.content = CONTENT_MISSING + flow = self.c.channel.ask("responseheaders", flow) + if flow == KILL: + raise KillSignal else: - flow.response.content = http.read_http_body( - self.c.server_conn.rfile, flow.response.headers, - self.c.config.body_size_limit, - flow.request.method, flow.response.code, False - ) - flow.response.timestamp_end = utils.timestamp() + # now get the rest of the request body, if body still needs to be + # read but not streaming this response + if flow.response.stream: + flow.response.content = CONTENT_MISSING + else: + flow.response.content = http.read_http_body( + self.c.server_conn.rfile, flow.response.headers, + self.c.config.body_size_limit, + flow.request.method, flow.response.code, False + ) + flow.response.timestamp_end = utils.timestamp() def handle_flow(self): flow = HTTPFlow(self.c.client_conn, self.c.server_conn, self.live) @@ -1092,8 +1098,16 @@ class HTTPHandler(ProtocolHandler): flow.live.restore_server() return True # Next flow please. - except (HttpAuthenticationError, http.HttpError, proxy.ProxyError, tcp.NetLibError), e: + except ( + HttpAuthenticationError, + http.HttpError, + proxy.ProxyError, + tcp.NetLibError, + ), e: self.handle_error(e, flow) + except KillSignal: + self.c.log("Connection killed", "info") + flow.live = None finally: flow.live = None # Connection is not live anymore. return False diff --git a/test/fuzzing/straight_stream b/test/fuzzing/straight_stream index 64feae450..a716a085a 100644 --- a/test/fuzzing/straight_stream +++ b/test/fuzzing/straight_stream @@ -1,4 +1,6 @@ mitmdump: $MITMDUMP -q --stream 1 pathod: $PATHOD -q -pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns \ No newline at end of file +#pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns +pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 /tmp/err + From 340d0570bfe7ceae68d7d592e3b7283480c351b0 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 26 Oct 2014 18:32:45 +1300 Subject: [PATCH 27/89] Legibility --- libmproxy/protocol/http.py | 172 +++++++++++++++++++------- test/fuzzing/straight_stream | 4 +- test/fuzzing/straight_stream_patterns | 2 + 3 files changed, 128 insertions(+), 50 deletions(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index e81c76406..3560f0bdb 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -27,7 +27,8 @@ def get_line(fp): Get a line, possibly preceded by a blank. """ line = fp.readline() - if line == "\r\n" or line == "\n": # Possible leftover from previous message + if line == "\r\n" or line == "\n": + # Possible leftover from previous message line = fp.readline() if line == "": raise tcp.NetLibDisconnect() @@ -241,25 +242,47 @@ class HTTPRequest(HTTPMessage): is content associated, but not present. CONTENT_MISSING evaluates to False to make checking for the presence of content natural. - form_in: The request form which mitmproxy has received. The following values are possible: - - relative (GET /index.html, OPTIONS *) (covers origin form and asterisk form) - - absolute (GET http://example.com:80/index.html) - - authority-form (CONNECT example.com:443) - Details: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-25#section-5.3 + form_in: The request form which mitmproxy has received. The following + values are possible: - form_out: The request form which mitmproxy has send out to the destination + - relative (GET /index.html, OPTIONS *) (covers origin form and + asterisk form) + - absolute (GET http://example.com:80/index.html) + - authority-form (CONNECT example.com:443) + Details: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-25#section-5.3 + + form_out: The request form which mitmproxy has send out to the + destination timestamp_start: Timestamp indicating when request transmission started timestamp_end: Timestamp indicating when request transmission ended """ - def __init__(self, form_in, method, scheme, host, port, path, httpversion, headers, - content, timestamp_start=None, timestamp_end=None, form_out=None): + def __init__( + self, + form_in, + method, + scheme, + host, + port, + path, + httpversion, + headers, + content, + timestamp_start=None, + timestamp_end=None, + form_out=None + ): assert isinstance(headers, ODictCaseless) or not headers - HTTPMessage.__init__(self, httpversion, headers, content, timestamp_start, - timestamp_end) - + HTTPMessage.__init__( + self, + httpversion, + headers, + content, + timestamp_start, + timestamp_end + ) self.form_in = form_in self.method = method self.scheme = scheme @@ -312,7 +335,8 @@ class HTTPRequest(HTTPMessage): request_line = get_line(rfile) - if hasattr(rfile, "first_byte_timestamp"): # more accurate timestamp_start + if hasattr(rfile, "first_byte_timestamp"): + # more accurate timestamp_start timestamp_start = rfile.first_byte_timestamp request_line_parts = http.parse_init(request_line) @@ -683,7 +707,9 @@ class HTTPResponse(HTTPMessage): return "".format( code=self.code, msg=self.msg, - contenttype=self.headers.get_first("content-type", "unknown content type"), + contenttype=self.headers.get_first( + "content-type", "unknown content type" + ), size=size ) @@ -704,7 +730,8 @@ class HTTPResponse(HTTPMessage): body_size_limit, include_body=include_body) - if hasattr(rfile, "first_byte_timestamp"): # more accurate timestamp_start + if hasattr(rfile, "first_byte_timestamp"): + # more accurate timestamp_start timestamp_start = rfile.first_byte_timestamp if include_body: @@ -745,7 +772,11 @@ class HTTPResponse(HTTPMessage): def _assemble_head(self, preserve_transfer_encoding=False): return '%s\r\n%s\r\n' % ( - self._assemble_first_line(), self._assemble_headers(preserve_transfer_encoding=preserve_transfer_encoding)) + self._assemble_first_line(), + self._assemble_headers( + preserve_transfer_encoding=preserve_transfer_encoding + ) + ) def assemble(self): """ @@ -755,7 +786,10 @@ class HTTPResponse(HTTPMessage): Raises an Exception if the request cannot be assembled. """ if self.content == CONTENT_MISSING: - raise proxy.ProxyError(502, "Cannot assemble flow with CONTENT_MISSING") + raise proxy.ProxyError( + 502, + "Cannot assemble flow with CONTENT_MISSING" + ) head = self._assemble_head() if self.content: return head + self.content @@ -822,8 +856,9 @@ class HTTPResponse(HTTPMessage): pairs = [pair.partition("=") for pair in header.split(';')] cookie_name = pairs[0][0] # the key of the first key/value pairs cookie_value = pairs[0][2] # the value of the first key/value pairs - cookie_parameters = {key.strip().lower(): value.strip() for key, sep, value in - pairs[1:]} + cookie_parameters = { + key.strip().lower(): value.strip() for key, sep, value in pairs[1:] + } cookies.append((cookie_name, (cookie_value, cookie_parameters))) return dict(cookies) @@ -856,7 +891,8 @@ class HTTPFlow(Flow): self.response = None """@type: HTTPResponse""" - self.intercepting = False # FIXME: Should that rather be an attribute of Flow? + # FIXME: Should that rather be an attribute of Flow? + self.intercepting = False _stateobject_attributes = Flow._stateobject_attributes.copy() _stateobject_attributes.update( @@ -944,7 +980,9 @@ class HTTPFlow(Flow): class HttpAuthenticationError(Exception): def __init__(self, auth_headers=None): - super(HttpAuthenticationError, self).__init__("Proxy Authentication Required") + super(HttpAuthenticationError, self).__init__( + "Proxy Authentication Required" + ) self.headers = auth_headers self.code = 407 @@ -1114,7 +1152,12 @@ class HTTPHandler(ProtocolHandler): def handle_server_reconnect(self, state): if state["state"] == "connect": - send_connect_request(self.c.server_conn, state["host"], state["port"], update_state=False) + send_connect_request( + self.c.server_conn, + state["host"], + state["port"], + update_state=False + ) else: # pragma: nocover raise RuntimeError("Unknown State: %s" % state["state"]) @@ -1138,11 +1181,11 @@ class HTTPHandler(ProtocolHandler): self.c.log(message_debug, level="debug") if flow: - # TODO: no flows without request or with both request and response at the moment. + # TODO: no flows without request or with both request and response + # at the moment. if flow.request and not flow.response: flow.error = Error(message or message_debug) self.c.channel.ask("error", flow) - try: code = getattr(error, "code", 502) headers = getattr(error, "headers", None) @@ -1156,12 +1199,22 @@ class HTTPHandler(ProtocolHandler): def send_error(self, code, message, headers): response = http_status.RESPONSES.get(code, "Unknown") - html_content = '\n%d %s\n\n\n%s\n\n' % \ - (code, response, message) + html_content = """ + + + %d %s + + + + """ % (code, response, message) self.c.client_conn.wfile.write("HTTP/1.1 %s %s\r\n" % (code, response)) - self.c.client_conn.wfile.write("Server: %s\r\n" % self.c.config.server_version) + self.c.client_conn.wfile.write( + "Server: %s\r\n" % self.c.config.server_version + ) self.c.client_conn.wfile.write("Content-type: text/html\r\n") - self.c.client_conn.wfile.write("Content-Length: %d\r\n" % len(html_content)) + self.c.client_conn.wfile.write( + "Content-Length: %d\r\n" % len(html_content) + ) if headers: for key, value in headers.items(): self.c.client_conn.wfile.write("%s: %s\r\n" % (key, value)) @@ -1201,11 +1254,15 @@ class HTTPHandler(ProtocolHandler): # Now we can process the request. if request.form_in == "authority": if self.c.client_conn.ssl_established: - raise http.HttpError(400, "Must not CONNECT on already encrypted connection") + raise http.HttpError( + 400, + "Must not CONNECT on already encrypted connection" + ) if self.c.config.mode == "regular": self.c.set_server_address((request.host, request.port)) - flow.server_conn = self.c.server_conn # Update server_conn attribute on the flow + # Update server_conn attribute on the flow + flow.server_conn = self.c.server_conn self.c.establish_server_connection() self.c.client_conn.send( 'HTTP/1.1 200 Connection established\r\n' + @@ -1217,7 +1274,9 @@ class HTTPHandler(ProtocolHandler): elif self.c.config.mode == "upstream": return None else: - pass # CONNECT should never occur if we don't expect absolute-form requests + # CONNECT should never occur if we don't expect absolute-form + # requests + pass elif request.form_in == self.expected_form_in: @@ -1225,61 +1284,77 @@ class HTTPHandler(ProtocolHandler): if request.form_in == "absolute": if request.scheme != "http": - raise http.HttpError(400, "Invalid request scheme: %s" % request.scheme) + raise http.HttpError( + 400, + "Invalid request scheme: %s" % request.scheme + ) if self.c.config.mode == "regular": - # Update info so that an inline script sees the correct value at flow.server_conn + # Update info so that an inline script sees the correct + # value at flow.server_conn self.c.set_server_address((request.host, request.port)) flow.server_conn = self.c.server_conn return None - - raise http.HttpError(400, "Invalid HTTP request form (expected: %s, got: %s)" % - (self.expected_form_in, request.form_in)) + raise http.HttpError( + 400, "Invalid HTTP request form (expected: %s, got: %s)" % ( + self.expected_form_in, request.form_in + ) + ) def process_server_address(self, flow): # Depending on the proxy mode, server handling is entirely different - # We provide a mostly unified API to the user, which needs to be unfiddled here + # We provide a mostly unified API to the user, which needs to be + # unfiddled here # ( See also: https://github.com/mitmproxy/mitmproxy/issues/337 ) address = netlib.tcp.Address((flow.request.host, flow.request.port)) ssl = (flow.request.scheme == "https") if self.c.config.mode == "upstream": - - # The connection to the upstream proxy may have a state we may need to take into account. + # The connection to the upstream proxy may have a state we may need + # to take into account. connected_to = None for s in flow.server_conn.state: if s[0] == "http" and s[1]["state"] == "connect": connected_to = tcp.Address((s[1]["host"], s[1]["port"])) - # We need to reconnect if the current flow either requires a (possibly impossible) - # change to the connection state, e.g. the host has changed but we already CONNECTed somewhere else. + # We need to reconnect if the current flow either requires a + # (possibly impossible) change to the connection state, e.g. the + # host has changed but we already CONNECTed somewhere else. needs_server_change = ( ssl != self.c.server_conn.ssl_established or - (connected_to and address != connected_to) # HTTP proxying is "stateless", CONNECT isn't. + # HTTP proxying is "stateless", CONNECT isn't. + (connected_to and address != connected_to) ) if needs_server_change: # force create new connection to the proxy server to reset state self.live.change_server(self.c.server_conn.address, force=True) if ssl: - send_connect_request(self.c.server_conn, address.host, address.port) + send_connect_request( + self.c.server_conn, + address.host, + address.port + ) self.c.establish_ssl(server=True) else: - # If we're not in upstream mode, we just want to update the host and possibly establish TLS. - self.live.change_server(address, ssl=ssl) # this is a no op if the addresses match. + # If we're not in upstream mode, we just want to update the host and + # possibly establish TLS. This is a no op if the addresses match. + self.live.change_server(address, ssl=ssl) flow.server_conn = self.c.server_conn def send_response_to_client(self, flow): if not flow.response.stream: # no streaming: - # we already received the full response from the server and can send it to the client straight away. + # we already received the full response from the server and can send + # it to the client straight away. self.c.client_conn.send(flow.response.assemble()) else: # streaming: - # First send the headers and then transfer the response incrementally: + # First send the headers and then transfer the response + # incrementally: h = flow.response._assemble_head(preserve_transfer_encoding=True) self.c.client_conn.send(h) for chunk in http.read_http_body_chunked(self.c.server_conn.rfile, @@ -1293,7 +1368,8 @@ class HTTPHandler(ProtocolHandler): def check_close_connection(self, flow): """ - Checks if the connection should be closed depending on the HTTP semantics. Returns True, if so. + Checks if the connection should be closed depending on the HTTP + semantics. Returns True, if so. """ close_connection = ( http.connection_close(flow.request.httpversion, flow.request.headers) or diff --git a/test/fuzzing/straight_stream b/test/fuzzing/straight_stream index a716a085a..99af212ff 100644 --- a/test/fuzzing/straight_stream +++ b/test/fuzzing/straight_stream @@ -1,6 +1,6 @@ mitmdump: $MITMDUMP -q --stream 1 pathod: $PATHOD -q -#pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns -pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 /tmp/err +pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns +#pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 /tmp/err diff --git a/test/fuzzing/straight_stream_patterns b/test/fuzzing/straight_stream_patterns index f5ae06f2f..591bf20de 100644 --- a/test/fuzzing/straight_stream_patterns +++ b/test/fuzzing/straight_stream_patterns @@ -3,3 +3,5 @@ get:'http://localhost:9999/p/':s'200:b"foo"':ir,'a' get:'http://localhost:9999/p/':s'200:b"foo"':ir,'9' get:'http://localhost:9999/p/':s'200:b"foo"':ir,':' get:'http://localhost:9999/p/':s'200:b"foo"':ir,'"' +get:'http://localhost:9999/p/':s'200:b"foo"':ir,'-' +get:'http://localhost:9999/p/':s'200:b"foo"':dr From 3b0964f36555949d35659f306054876a49dbcfa1 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 30 Oct 2014 17:38:23 +0100 Subject: [PATCH 28/89] fix #391 --- libmproxy/proxy/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index fe2b45f4e..a228192a4 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -43,9 +43,9 @@ class ProxyConfig: self.body_size_limit = body_size_limit if mode == "transparent": - self.mode = TransparentProxyMode(platform.resolver(), TRANSPARENT_SSL_PORTS) + self.mode = TransparentProxyMode(platform.resolver(), ssl_ports) elif mode == "socks5": - self.mode = Socks5ProxyMode(TRANSPARENT_SSL_PORTS) + self.mode = Socks5ProxyMode(ssl_ports) elif mode == "reverse": self.mode = ReverseProxyMode(upstream_server) elif mode == "upstream": From 2c64b90a3d239c76bca4c334f89d7b53a965d35b Mon Sep 17 00:00:00 2001 From: Wade 524 Date: Fri, 31 Oct 2014 11:49:45 -0700 Subject: [PATCH 29/89] Adding some test coverage for handling HTTP OPTIONS requests. --- test/test_protocol_http.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/test/test_protocol_http.py b/test/test_protocol_http.py index ea6cf3fd7..f5b9e0a1e 100644 --- a/test/test_protocol_http.py +++ b/test/test_protocol_http.py @@ -23,7 +23,7 @@ def test_stripped_chunked_encoding_no_content(): class TestHTTPRequest: - def test_asterisk_form(self): + def test_asterisk_form_in(self): s = StringIO("OPTIONS * HTTP/1.1") f = tutils.tflow(req=None) f.request = HTTPRequest.from_stream(s) @@ -33,7 +33,7 @@ class TestHTTPRequest: f.request.scheme = "http" assert f.request.assemble() == "OPTIONS * HTTP/1.1\r\nHost: address:22\r\n\r\n" - def test_origin_form(self): + def test_relative_form_in(self): s = StringIO("GET /foo\xff HTTP/1.1") tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s) s = StringIO("GET /foo HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: h2c") @@ -52,8 +52,7 @@ class TestHTTPRequest: r.update_host_header() assert "Host" in r.headers - - def test_authority_form(self): + def test_authority_form_in(self): s = StringIO("CONNECT oops-no-port.com HTTP/1.1") tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s) s = StringIO("CONNECT address:22 HTTP/1.1") @@ -62,13 +61,37 @@ class TestHTTPRequest: assert r.assemble() == "CONNECT address:22 HTTP/1.1\r\nHost: address:22\r\n\r\n" assert r.pretty_url(False) == "address:22" - def test_absolute_form(self): + def test_absolute_form_in(self): s = StringIO("GET oops-no-protocol.com HTTP/1.1") tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s) s = StringIO("GET http://address:22/ HTTP/1.1") r = HTTPRequest.from_stream(s) assert r.assemble() == "GET http://address:22/ HTTP/1.1\r\nHost: address:22\r\n\r\n" + def test_http_options_relative_form_in(self): + """ + Exercises fix for Issue #xxx. + """ + s = StringIO("OPTIONS /secret/resource HTTP/1.1") + r = HTTPRequest.from_stream(s) + r.host = 'address' + r.port = 80 + r.scheme = "http" + assert r.assemble() == ("OPTIONS " + "/secret/resource " + "HTTP/1.1\r\nHost: address\r\n\r\n") + + def test_http_options_absolute_form_in(self): + s = StringIO("OPTIONS http://address/secret/resource HTTP/1.1") + r = HTTPRequest.from_stream(s) + r.host = 'address' + r.port = 80 + r.scheme = "http" + assert r.assemble() == ("OPTIONS " + "http://address:80/secret/resource " + "HTTP/1.1\r\nHost: address\r\n\r\n") + + def test_assemble_unknown_form(self): r = tutils.treq() tutils.raises("Invalid request form", r.assemble, "antiauthority") @@ -133,4 +156,4 @@ class TestInvalidRequests(tservers.HTTPProxTest): p.connect() r = p.request("get:/p/200") assert r.status_code == 400 - assert "Invalid HTTP request form" in r.content \ No newline at end of file + assert "Invalid HTTP request form" in r.content From ce18cd8ba40998c0654e697efcc0a0f018e45375 Mon Sep 17 00:00:00 2001 From: Wade 524 Date: Fri, 31 Oct 2014 11:50:03 -0700 Subject: [PATCH 30/89] Fixing issue #392. --- libmproxy/protocol/http.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 3560f0bdb..1472f2cac 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -251,7 +251,7 @@ class HTTPRequest(HTTPMessage): - authority-form (CONNECT example.com:443) Details: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-25#section-5.3 - form_out: The request form which mitmproxy has send out to the + form_out: The request form which mitmproxy will send out to the destination timestamp_start: Timestamp indicating when request transmission started @@ -401,9 +401,8 @@ class HTTPRequest(HTTPMessage): form = form or self.form_out if form == "relative": - path = self.path if self.method != "OPTIONS" else "*" request_line = '%s %s HTTP/%s.%s' % ( - self.method, path, self.httpversion[0], self.httpversion[1] + self.method, self.path, self.httpversion[0], self.httpversion[1] ) elif form == "authority": request_line = '%s %s:%s HTTP/%s.%s' % ( From c4c42fa040f4e0177516e9830591ca36a3660f3f Mon Sep 17 00:00:00 2001 From: Wade 524 Date: Fri, 31 Oct 2014 12:45:31 -0700 Subject: [PATCH 31/89] Updating OPTIONS test with related issue number. --- test/test_protocol_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_protocol_http.py b/test/test_protocol_http.py index f5b9e0a1e..db2629508 100644 --- a/test/test_protocol_http.py +++ b/test/test_protocol_http.py @@ -70,7 +70,7 @@ class TestHTTPRequest: def test_http_options_relative_form_in(self): """ - Exercises fix for Issue #xxx. + Exercises fix for Issue #392. """ s = StringIO("OPTIONS /secret/resource HTTP/1.1") r = HTTPRequest.from_stream(s) From d0de490ef1ced7597471c1867d30213b162a7e89 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 2 Nov 2014 18:04:57 +1300 Subject: [PATCH 32/89] Release prep: binaries build script, release checklist, fuzzing --- release/osx-binaries | 3 ++- release/release-checklist | 4 ++++ test/fuzzing/.env | 2 +- test/fuzzing/straight_stream | 6 +++--- test/fuzzing/straight_stream_patterns | 12 +++++++++++- test/fuzzing/straight_stream_ssl | 6 ++++++ 6 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 test/fuzzing/straight_stream_ssl diff --git a/release/osx-binaries b/release/osx-binaries index 4be85800e..9945e471b 100755 --- a/release/osx-binaries +++ b/release/osx-binaries @@ -10,7 +10,8 @@ # answer is to touch the __init__.py file in the zope directory. On my system: # touch /Library/Python/2.7/site-packages/zope/__init__.py -# To run, change into the pyinstaller directory, and then run this script. +# To run, first install netlib and mitmproxy, then change into the pyinstaller +# directory, and then run this script. DST=/tmp/osx-mitmproxy MITMPROXY=~/mitmproxy/mitmproxy diff --git a/release/release-checklist b/release/release-checklist index d0bf8aad4..683e9b895 100644 --- a/release/release-checklist +++ b/release/release-checklist @@ -26,3 +26,7 @@ - tar -xzvf pkgfile.tgz - virtualenv venv +- Build the OSX binaries + - Follow instructions in osxbinaries + + diff --git a/test/fuzzing/.env b/test/fuzzing/.env index e2cf7829e..82ae6a8d2 100644 --- a/test/fuzzing/.env +++ b/test/fuzzing/.env @@ -2,5 +2,5 @@ MITMDUMP=../../mitmdump PATHOD=../../../pathod/pathod PATHOC=../../../pathod/pathoc -FUZZ_SETTINGS=-remTt 1 -n 0 -I 200,400,405,502 +FUZZ_SETTINGS=-remTt 1 -n 0 diff --git a/test/fuzzing/straight_stream b/test/fuzzing/straight_stream index 99af212ff..41e2a6e16 100644 --- a/test/fuzzing/straight_stream +++ b/test/fuzzing/straight_stream @@ -1,6 +1,6 @@ -mitmdump: $MITMDUMP -q --stream 1 -pathod: $PATHOD -q +mitmdump: $MITMDUMP +pathod: $PATHOD pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns -#pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 /tmp/err +#pathoc: sleep 2 && $PATHOC localhost:8080 /tmp/err diff --git a/test/fuzzing/straight_stream_patterns b/test/fuzzing/straight_stream_patterns index 591bf20de..93a066e6e 100644 --- a/test/fuzzing/straight_stream_patterns +++ b/test/fuzzing/straight_stream_patterns @@ -4,4 +4,14 @@ get:'http://localhost:9999/p/':s'200:b"foo"':ir,'9' get:'http://localhost:9999/p/':s'200:b"foo"':ir,':' get:'http://localhost:9999/p/':s'200:b"foo"':ir,'"' get:'http://localhost:9999/p/':s'200:b"foo"':ir,'-' -get:'http://localhost:9999/p/':s'200:b"foo"':dr + +get:'http://localhost:9999/p/':s'200:b"foo":ir,"\n"' +get:'http://localhost:9999/p/':s'200:b"foo":ir,"a"' +get:'http://localhost:9999/p/':s'200:b"foo":ir,"9"' +get:'http://localhost:9999/p/':s'200:b"foo":ir,":"' +get:'http://localhost:9999/p/':s"200:b'foo':ir,'\"'" +get:'http://localhost:9999/p/':s'200:b"foo":ir,"-"' +get:'http://localhost:9999/p/':s'200:b"foo":dr' + +get:'http://localhost:9999/p/':s'200:b"foo"':ir,@2 +get:'http://localhost:9999/p/':s'200:b"foo":ir,@2' diff --git a/test/fuzzing/straight_stream_ssl b/test/fuzzing/straight_stream_ssl new file mode 100644 index 000000000..708ff0b3d --- /dev/null +++ b/test/fuzzing/straight_stream_ssl @@ -0,0 +1,6 @@ + +mitmdump: $MITMDUMP -q --stream 1 +pathod: $PATHOD +pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns +#pathoc: sleep 2 && $PATHOC localhost:8080 /tmp/err + From e732771c1ca354ef7b895da4088e727573476e34 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Wed, 5 Nov 2014 09:57:09 +1300 Subject: [PATCH 33/89] We don't need requests for mitmproxy --- doc-src/install.html | 2 -- setup.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/doc-src/install.html b/doc-src/install.html index 8a76e3ae9..682e317e6 100644 --- a/doc-src/install.html +++ b/doc-src/install.html @@ -1,6 +1,4 @@ - - ## Installing from source The preferred way to install mitmproxy - whether you're installing the latest diff --git a/setup.py b/setup.py index d6983d4fc..ead61118c 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ if os.name != "nt": deps = { "netlib>=%s" % version.MINORVERSION, "pyasn1>0.1.2", - "requests>=2.4.0", "pyOpenSSL>=0.14", "Flask>=0.10.1", "tornado>=4.0.2", @@ -90,4 +89,4 @@ setup( "cssutils>=1.0" ] } -) \ No newline at end of file +) From 0fe83ce87bd1e3cf00fb06d2cc06e1bf3b0dbe85 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 6 Nov 2014 10:35:00 +1300 Subject: [PATCH 34/89] Fix bug in flow dumping, add unit test that should have caught this in the first place --- libmproxy/dump.py | 2 +- test/test_dump.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/libmproxy/dump.py b/libmproxy/dump.py index 9fb0f0017..4d899fe8a 100644 --- a/libmproxy/dump.py +++ b/libmproxy/dump.py @@ -205,7 +205,7 @@ class DumpMaster(flow.FlowMaster): elif self.o.flow_detail >= 3: print >> self.outfile, str_request(f, self.showhost) print >> self.outfile, self.indent(4, f.request.headers) - if utils.isBin(f.request.content): + if f.request.content != http.CONTENT_MISSING and utils.isBin(f.request.content): d = netlib.utils.hexdump(f.request.content) d = "\n".join("%s\t%s %s"%i for i in d) print >> self.outfile, self.indent(4, d) diff --git a/test/test_dump.py b/test/test_dump.py index 2e58e073a..e9cb4d331 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -1,10 +1,12 @@ import os from cStringIO import StringIO -from libmproxy import dump, flow, proxy +from libmproxy import dump, flow +from libmproxy.protocol import http from libmproxy.proxy.primitives import Log import tutils import mock + def test_strfuncs(): t = tutils.tresp() t.is_replay = True @@ -58,6 +60,18 @@ class TestDumpMaster: assert m.handle_error(f) assert "error" in cs.getvalue() + def test_missing_content(self): + cs = StringIO() + o = dump.Options(flow_detail=3) + m = dump.DumpMaster(None, o, outfile=cs) + f = tutils.tflow() + f.request.content = http.CONTENT_MISSING + m.handle_request(f) + f.response = tutils.tresp() + f.response.content = http.CONTENT_MISSING + m.handle_response(f) + assert "content missing" in cs.getvalue() + def test_replay(self): cs = StringIO() From a2a87695d32df6bcf31a0785496d8ca6e41423c8 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 6 Nov 2014 10:51:30 +1300 Subject: [PATCH 35/89] Reduce loop timeouts to improve mitmproxy responsiveness Fixes #384 --- libmproxy/console/__init__.py | 2 +- libmproxy/flow.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index cb6a977f1..ffd9eda88 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -819,7 +819,7 @@ class ConsoleMaster(flow.FlowMaster): self.statusbar.redraw() size = self.drawscreen() changed = self.tick(self.masterq, 0.01) - self.ui.set_input_timeouts(max_wait=0.1) + self.ui.set_input_timeouts(max_wait=0.01) keys = self.ui.get_input() if keys: changed = True diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 6136ec1c8..bd35e864e 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -27,7 +27,12 @@ class AppRegistry: 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) + self.apps[(domain, port)] = wsgi.WSGIAdaptor( + app, + domain, + port, + version.NAMEVERSION + ) def get(self, request): """ @@ -72,7 +77,8 @@ class ReplaceHooks: def get_specs(self): """ - Retrieve the hook specifcations. Returns a list of (fpatt, rex, s) tuples. + Retrieve the hook specifcations. Returns a list of (fpatt, rex, s) + tuples. """ return [i[:3] for i in self.lst] @@ -119,7 +125,8 @@ class SetHeaders: def get_specs(self): """ - Retrieve the hook specifcations. Returns a list of (fpatt, rex, s) tuples. + Retrieve the hook specifcations. Returns a list of (fpatt, rex, s) + tuples. """ return [i[:3] for i in self.lst] From dc142682cb930cb3903a2fc66d4785bd5367360b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 6 Nov 2014 11:25:03 +0100 Subject: [PATCH 36/89] fix #399 --- libmproxy/console/__init__.py | 3 ++- libmproxy/console/flowlist.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index ffd9eda88..fc6600c1e 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -516,7 +516,8 @@ class ConsoleMaster(flow.FlowMaster): self.start_server_playback( ret, self.killextra, self.rheaders, - False, self.nopop + False, self.nopop, + self.options.replay_ignore_params, self.options.replay_ignore_content ) def spawn_editor(self, data): diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index e03301712..3eb4eb1a5 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -120,13 +120,15 @@ class ConnectionItem(common.WWrap): self.master.start_server_playback( [i.copy() for i in self.master.state.view], self.master.killextra, self.master.rheaders, - False, self.master.nopop + False, self.master.nopop, + self.master.options.replay_ignore_params, self.master.options.replay_ignore_content ) elif k == "t": self.master.start_server_playback( [self.flow.copy()], self.master.killextra, self.master.rheaders, - False, self.master.nopop + False, self.master.nopop, + self.master.options.replay_ignore_params, self.master.options.replay_ignore_content ) else: self.master.path_prompt( From 5025bf872c4ea407785ed1c0fbd0b7703ccd2a04 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 7 Nov 2014 16:32:46 +1300 Subject: [PATCH 37/89] Minor doc adjustments --- CHANGELOG | 2 ++ release/release-checklist | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c78fdccec..2df1c839d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ 23 October 2014: mitmproxy 0.11: + + * Performance improvements for mitmproxy console * SOCKS5 proxy mode allows mitmproxy to act as a SOCKS5 proxy server diff --git a/release/release-checklist b/release/release-checklist index 683e9b895..4ec74e7a9 100644 --- a/release/release-checklist +++ b/release/release-checklist @@ -2,8 +2,6 @@ - Bump the version number: mitmproxy/libmproxy/version.py - mitmproxy/requirements.txt - mitmproxy/test/requirements.txt netlib/netlib/version.py netlib/requirements.txt netlib/test/requirements.txt From c3ec5515466fe7ba970bc7cb2578dad4c845e5bc Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Nov 2014 09:52:46 +0100 Subject: [PATCH 38/89] fix #401 --- libmproxy/protocol/http.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 1472f2cac..c8974d251 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -434,11 +434,9 @@ class HTTPRequest(HTTPMessage): self.host, self.port)] - if self.content: + # If content is defined (i.e. not None or CONTENT_MISSING), we always add a content-length header. + if self.content or self.content == "": headers["Content-Length"] = [str(len(self.content))] - elif 'Transfer-Encoding' in self.headers: - # content-length for e.g. chuncked transfer-encoding with no content - headers["Content-Length"] = ["0"] return str(headers) @@ -761,11 +759,9 @@ class HTTPResponse(HTTPMessage): if not preserve_transfer_encoding: del headers['Transfer-Encoding'] - if self.content: + # If content is defined (i.e. not None or CONTENT_MISSING), we always add a content-length header. + if self.content or self.content == "": headers["Content-Length"] = [str(len(self.content))] - # add content-length for chuncked transfer-encoding with no content - elif not preserve_transfer_encoding and 'Transfer-Encoding' in self.headers: - headers["Content-Length"] = ["0"] return str(headers) From 4d090e09c7b6440176a22b541e37a0b1a0a08570 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Nov 2014 09:59:11 +0100 Subject: [PATCH 39/89] fix tests --- test/test_protocol_http.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/test_protocol_http.py b/test/test_protocol_http.py index db2629508..16870777d 100644 --- a/test/test_protocol_http.py +++ b/test/test_protocol_http.py @@ -31,7 +31,9 @@ class TestHTTPRequest: f.request.host = f.server_conn.address.host f.request.port = f.server_conn.address.port f.request.scheme = "http" - assert f.request.assemble() == "OPTIONS * HTTP/1.1\r\nHost: address:22\r\n\r\n" + assert f.request.assemble() == ("OPTIONS * HTTP/1.1\r\n" + "Host: address:22\r\n" + "Content-Length: 0\r\n\r\n") def test_relative_form_in(self): s = StringIO("GET /foo\xff HTTP/1.1") @@ -58,7 +60,9 @@ class TestHTTPRequest: s = StringIO("CONNECT address:22 HTTP/1.1") r = HTTPRequest.from_stream(s) r.scheme, r.host, r.port = "http", "address", 22 - assert r.assemble() == "CONNECT address:22 HTTP/1.1\r\nHost: address:22\r\n\r\n" + assert r.assemble() == ("CONNECT address:22 HTTP/1.1\r\n" + "Host: address:22\r\n" + "Content-Length: 0\r\n\r\n") assert r.pretty_url(False) == "address:22" def test_absolute_form_in(self): @@ -66,7 +70,7 @@ class TestHTTPRequest: tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s) s = StringIO("GET http://address:22/ HTTP/1.1") r = HTTPRequest.from_stream(s) - assert r.assemble() == "GET http://address:22/ HTTP/1.1\r\nHost: address:22\r\n\r\n" + assert r.assemble() == "GET http://address:22/ HTTP/1.1\r\nHost: address:22\r\nContent-Length: 0\r\n\r\n" def test_http_options_relative_form_in(self): """ @@ -77,9 +81,9 @@ class TestHTTPRequest: r.host = 'address' r.port = 80 r.scheme = "http" - assert r.assemble() == ("OPTIONS " - "/secret/resource " - "HTTP/1.1\r\nHost: address\r\n\r\n") + assert r.assemble() == ("OPTIONS /secret/resource HTTP/1.1\r\n" + "Host: address\r\n" + "Content-Length: 0\r\n\r\n") def test_http_options_absolute_form_in(self): s = StringIO("OPTIONS http://address/secret/resource HTTP/1.1") @@ -87,9 +91,9 @@ class TestHTTPRequest: r.host = 'address' r.port = 80 r.scheme = "http" - assert r.assemble() == ("OPTIONS " - "http://address:80/secret/resource " - "HTTP/1.1\r\nHost: address\r\n\r\n") + assert r.assemble() == ("OPTIONS http://address:80/secret/resource HTTP/1.1\r\n" + "Host: address\r\n" + "Content-Length: 0\r\n\r\n") def test_assemble_unknown_form(self): From 6f5883a4d19caee30a89729ef5f0f4e50da8359c Mon Sep 17 00:00:00 2001 From: Lucas Cimon Date: Fri, 7 Nov 2014 17:01:58 +0100 Subject: [PATCH 40/89] Using uppercase C to 'clear' display mode, because lowercase 'c' is used for css --- libmproxy/console/flowview.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index b2c461475..bf0070fc6 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -637,11 +637,10 @@ class FlowView(common.WWrap): return self._view_nextprev_flow("prev", flow) def change_this_display_mode(self, t): - self.state.add_flow_setting( - self.flow, - (self.state.view_flow_mode, "prettyview"), - contentview.get_by_shortcut(t) - ) + key = (self.state.view_flow_mode, "prettyview") + value = contentview.get_by_shortcut(t) + if value: + self.state.add_flow_setting(self.flow, key, value) self.master.refresh_flow(self.flow) def delete_body(self, t): @@ -749,7 +748,7 @@ class FlowView(common.WWrap): self.master.statusbar.message("") elif key == "m": p = list(contentview.view_prompts) - p.insert(0, ("clear", "c")) + p.insert(0, ("Clear", "C")) self.master.prompt_onekey( "Display mode", p, From 476d7da17c7d22415cbd16b625ba8e443a750f0f Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 10 Nov 2014 15:51:26 +0100 Subject: [PATCH 41/89] update change_upstream_proxy example --- examples/change_upstream_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/change_upstream_proxy.py b/examples/change_upstream_proxy.py index e063ca4fa..74a43bd0d 100644 --- a/examples/change_upstream_proxy.py +++ b/examples/change_upstream_proxy.py @@ -1,7 +1,7 @@ -# This scripts demonstrates how mitmproxy can switch to a different upstream proxy +# This scripts demonstrates how mitmproxy can switch to a second/different upstream proxy # in upstream proxy mode. # -# Usage: mitmdump -s "change_upstream_proxy.py host" +# Usage: mitmdump -U http://default-upstream-proxy.local:8080/ -s "change_upstream_proxy.py host" from libmproxy.protocol.http import send_connect_request alternative_upstream_proxy = ("localhost", 8082) From cece3700df02144dc2bd26a55fe4d6076fd7ea22 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 10 Nov 2014 17:11:36 +0100 Subject: [PATCH 42/89] fix #402 --- libmproxy/flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index bd35e864e..1826af3de 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -192,6 +192,7 @@ class ClientPlaybackState: """ if self.flows and not self.current: n = self.flows.pop(0) + n.response = None n.reply = controller.DummyReply() self.current = master.handle_request(n) if not testing and not self.current.response: @@ -615,7 +616,7 @@ class FlowMaster(controller.Master): ] if all(e): self.shutdown() - self.client_playback.tick(self, timeout) + self.client_playback.tick(self) return controller.Master.tick(self, q, timeout) From 6f3b4eee3c3e30b391be457e38fb5ac67f8ef682 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 10 Nov 2014 17:35:28 +0100 Subject: [PATCH 43/89] fix clear key --- libmproxy/console/flowview.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index bf0070fc6..3dceff704 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -637,10 +637,11 @@ class FlowView(common.WWrap): return self._view_nextprev_flow("prev", flow) def change_this_display_mode(self, t): - key = (self.state.view_flow_mode, "prettyview") - value = contentview.get_by_shortcut(t) - if value: - self.state.add_flow_setting(self.flow, key, value) + self.state.add_flow_setting( + self.flow, + (self.state.view_flow_mode, "prettyview"), + contentview.get_by_shortcut(t) + ) self.master.refresh_flow(self.flow) def delete_body(self, t): From ec17e70d9e82bc6ae62300397b2f849c2d8dcc85 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Tue, 11 Nov 2014 14:00:28 +1300 Subject: [PATCH 44/89] Remove entry points in favor of vanilla scripts, fix test-release --- release/test-release | 15 ++++++++------- setup.py | 11 +---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/release/test-release b/release/test-release index 8b53c14c5..8cbcea8c5 100755 --- a/release/test-release +++ b/release/test-release @@ -23,15 +23,16 @@ python ./setup.py -q sdist --dist-dir $DST echo "Creating virtualenv for test install..." virtualenv -q $DST/venv +cd $DST echo "Installing netlib..." -$DST/venv/bin/pip -q install --download-cache ~/.pipcache $DST/netlib* +./venv/bin/pip -q install --download-cache ~/.pipcache ./netlib* echo "Installing pathod..." -$DST/venv/bin/pip -q install --download-cache ~/.pipcache $DST/pathod* +./venv/bin/pip -q install --download-cache ~/.pipcache ./pathod* echo "Installing mitmproxy..." -$DST/venv/bin/pip -q install --download-cache ~/.pipcache $DST/mitmproxy* +./venv/bin/pip -q install --download-cache ~/.pipcache ./mitmproxy* echo "Running binaries..." -$DST/venv/bin/mitmproxy --version -$DST/venv/bin/mitmdump --version -$DST/venv/bin/pathod --version -$DST/venv/bin/pathoc --version +./venv/bin/mitmproxy --version +./venv/bin/mitmdump --version +./venv/bin/pathod --version +./venv/bin/pathoc --version diff --git a/setup.py b/setup.py index ead61118c..e9d8f8cba 100644 --- a/setup.py +++ b/setup.py @@ -36,10 +36,6 @@ for script in scripts: if os.name == "nt": deps.add("pydivert>=0.0.4") # Transparent proxying on Windows -console_scripts = [ - "%s = libmproxy.main:%s" % (s, s) for s in scripts -] - setup( name="mitmproxy", @@ -66,14 +62,9 @@ setup( "Topic :: Internet :: Proxy Servers", "Topic :: Software Development :: Testing" ], - packages=find_packages(), include_package_data=True, - - entry_points={ - 'console_scripts': console_scripts - }, - + scripts = scripts, install_requires=list(deps), extras_require={ 'dev': [ From f19ee74b99d0049f4789673e202cfc3fda59bb04 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 11 Nov 2014 12:30:51 +0100 Subject: [PATCH 45/89] be more explicit about requirements --- libmproxy/version.py | 4 ++++ setup.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libmproxy/version.py b/libmproxy/version.py index 3483625de..f4c0cd94e 100644 --- a/libmproxy/version.py +++ b/libmproxy/version.py @@ -3,3 +3,7 @@ VERSION = ".".join(str(i) for i in IVERSION) MINORVERSION = ".".join(str(i) for i in IVERSION[:2]) NAME = "mitmproxy" NAMEVERSION = NAME + " " + VERSION + +NEXT_MINORVERSION = list(IVERSION) +NEXT_MINORVERSION[1] += 1 +NEXT_MINORVERSION = ".".join(str(i) for i in NEXT_MINORVERSION[:2]) \ No newline at end of file diff --git a/setup.py b/setup.py index ead61118c..149e544a9 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ if os.name != "nt": scripts.append("mitmproxy") deps = { - "netlib>=%s" % version.MINORVERSION, + "netlib>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION), "pyasn1>0.1.2", "pyOpenSSL>=0.14", "Flask>=0.10.1", @@ -81,7 +81,7 @@ setup( "nose>=1.3.0", "nose-cov>=1.6", "coveralls>=0.4.1", - "pathod>=%s" % version.MINORVERSION + "pathod>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION) ], 'contentviews': [ "pyamf>=0.6.1", From a325ae638b07a8a08018403e57c05ab8d4119161 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 11 Nov 2014 13:09:05 +0100 Subject: [PATCH 46/89] fix tests --- libmproxy/flow.py | 2 +- test/test_flow.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 1826af3de..3d5a6a360 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -191,7 +191,7 @@ class ClientPlaybackState: testing: Disables actual replay for testing. """ if self.flows and not self.current: - n = self.flows.pop(0) + n = self.flows.pop(0).copy() n.response = None n.reply = controller.DummyReply() self.current = master.handle_request(n) diff --git a/test/test_flow.py b/test/test_flow.py index 6ed279c21..8c197153d 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -692,7 +692,8 @@ class TestFlowMaster: f = tutils.tflow(resp=True) pb = [tutils.tflow(resp=True), f] - fm = flow.FlowMaster(None, s) + + fm = flow.FlowMaster(DummyServer(ProxyConfig()), s) assert not fm.start_server_playback(pb, False, [], False, False, None, False) assert not fm.start_client_playback(pb, False) From 8cbb67ac70772ac141fe1891e5fd47dd1cbc978c Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 13 Nov 2014 23:03:06 +0100 Subject: [PATCH 47/89] docs++ --- doc-src/ssl.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc-src/ssl.html b/doc-src/ssl.html index 16aed1dcf..3fa0e0700 100644 --- a/doc-src/ssl.html +++ b/doc-src/ssl.html @@ -71,6 +71,15 @@ For example, you can generate a certificate in this format using these instructi +Using a custom certificate authority +------------------------------------ + +By default, mitmproxy will (generate and) use ~/.mitmproxy/mitmproxy-ca.pem as the default certificate +authority to generate certificates for all domains for which no custom certificate is provided (see above). +You can use your own certificate authority by passing the --confdir option to mitmproxy. +mitmproxy will then look for mitmproxy-ca.pem in the specified directory. If no such file exists, +it will be generated automatically. + Installing the mitmproxy CA --------------------------- From 9b5a8af12d820fd3593424989759568e7cffd596 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 14 Nov 2014 00:21:47 +0100 Subject: [PATCH 48/89] fix grideditor bug --- libmproxy/console/grideditor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index 1673d536b..72c1e4a05 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -129,6 +129,8 @@ class GridWalker(urwid.ListWalker): if emsg: self.editor.master.statusbar.message(emsg, 1000) errors.add(self.focus_col) + else: + errors.discard(self.focus_col) row = list(self.lst[self.focus][0]) row[self.focus_col] = val From 0c52b4e3b9137e22f6c8c649d6b584d44d7f4b75 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 14 Nov 2014 00:26:22 +0100 Subject: [PATCH 49/89] handle script hooks in replay, fix tests, fix #402 --- libmproxy/flow.py | 24 +++++++------- libmproxy/protocol/http.py | 64 ++++++++++++++++++++++---------------- test/test_flow.py | 8 +++-- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 3d5a6a360..007136985 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -169,6 +169,7 @@ class ClientPlaybackState: def __init__(self, flows, exit): self.flows, self.exit = flows, exit self.current = None + self.testing = False # Disables actual replay for testing. def count(self): return len(self.flows) @@ -186,19 +187,16 @@ class ClientPlaybackState: if flow is self.current: self.current = None - def tick(self, master, testing=False): - """ - testing: Disables actual replay for testing. - """ + def tick(self, master): if self.flows and not self.current: - n = self.flows.pop(0).copy() - n.response = None - n.reply = controller.DummyReply() - self.current = master.handle_request(n) - if not testing and not self.current.response: - master.replay_request(self.current) # pragma: no cover - elif self.current.response: - master.handle_response(self.current) + self.current = self.flows.pop(0).copy() + if not self.testing: + master.replay_request(self.current) + else: + self.current.reply = controller.DummyReply() + master.handle_request(self.current) + if self.current.response: + master.handle_response(self.current) class ServerPlaybackState: @@ -371,6 +369,8 @@ class State(object): """ Add a request to the state. Returns the matching flow. """ + if flow in self._flow_list: # catch flow replay + return flow self._flow_list.append(flow) if flow.match(self._limit): self.view.append(flow) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index c8974d251..26a94040c 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1040,7 +1040,7 @@ class HTTPHandler(ProtocolHandler): # inline script to set flow.stream = True flow = self.c.channel.ask("responseheaders", flow) if flow == KILL: - raise KillSignal + raise KillSignal() else: # now get the rest of the request body, if body still needs to be # read but not streaming this response @@ -1085,7 +1085,7 @@ class HTTPHandler(ProtocolHandler): self.process_server_address(flow) # The inline script may have changed request.host if request_reply is None or request_reply == KILL: - return False + raise KillSignal() if isinstance(request_reply, HTTPResponse): flow.response = request_reply @@ -1099,7 +1099,7 @@ class HTTPHandler(ProtocolHandler): self.c.log("response", "debug", [flow.response._assemble_first_line()]) response_reply = self.c.channel.ask("response", flow) if response_reply is None or response_reply == KILL: - return False + raise KillSignal() self.send_response_to_client(flow) @@ -1140,7 +1140,6 @@ class HTTPHandler(ProtocolHandler): self.handle_error(e, flow) except KillSignal: self.c.log("Connection killed", "info") - flow.live = None finally: flow.live = None # Connection is not live anymore. return False @@ -1437,32 +1436,43 @@ class RequestReplayThread(threading.Thread): r = self.flow.request form_out_backup = r.form_out try: - # In all modes, we directly connect to the server displayed - if self.config.mode == "upstream": - server_address = self.config.mode.get_upstream_server(self.flow.client_conn)[2:] - server = ServerConnection(server_address) - server.connect() - if r.scheme == "https": - send_connect_request(server, r.host, r.port) - server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) - r.form_out = "relative" - else: - r.form_out = "absolute" + self.flow.response = None + request_reply = self.channel.ask("request", self.flow) + if request_reply is None or request_reply == KILL: + raise KillSignal() + elif isinstance(request_reply, HTTPResponse): + self.flow.response = request_reply else: - server_address = (r.host, r.port) - server = ServerConnection(server_address) - server.connect() - if r.scheme == "https": - server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) - r.form_out = "relative" + # In all modes, we directly connect to the server displayed + if self.config.mode == "upstream": + server_address = self.config.mode.get_upstream_server(self.flow.client_conn)[2:] + server = ServerConnection(server_address) + server.connect() + if r.scheme == "https": + send_connect_request(server, r.host, r.port) + server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) + r.form_out = "relative" + else: + r.form_out = "absolute" + else: + server_address = (r.host, r.port) + server = ServerConnection(server_address) + server.connect() + if r.scheme == "https": + server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni) + r.form_out = "relative" - server.send(r.assemble()) - self.flow.server_conn = server - self.flow.response = HTTPResponse.from_stream(server.rfile, r.method, - body_size_limit=self.config.body_size_limit) - self.channel.ask("response", self.flow) - except (proxy.ProxyError, http.HttpError, tcp.NetLibError), v: + server.send(r.assemble()) + self.flow.server_conn = server + self.flow.response = HTTPResponse.from_stream(server.rfile, r.method, + body_size_limit=self.config.body_size_limit) + response_reply = self.channel.ask("response", self.flow) + if response_reply is None or response_reply == KILL: + raise KillSignal() + except (proxy.ProxyError, http.HttpError, tcp.NetLibError) as v: self.flow.error = Error(repr(v)) self.channel.ask("error", self.flow) + except KillSignal: + self.channel.tell("log", proxy.Log("Connection killed", "info")) finally: r.form_out = form_out_backup diff --git a/test/test_flow.py b/test/test_flow.py index 8c197153d..22abb4d41 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -86,19 +86,20 @@ class TestClientPlaybackState: fm = flow.FlowMaster(None, s) fm.start_client_playback([first, tutils.tflow()], True) c = fm.client_playback + c.testing = True assert not c.done() assert not s.flow_count() assert c.count() == 2 - c.tick(fm, testing=True) + c.tick(fm) assert s.flow_count() assert c.count() == 1 - c.tick(fm, testing=True) + c.tick(fm) assert c.count() == 1 c.clear(c.current) - c.tick(fm, testing=True) + c.tick(fm) assert c.count() == 0 c.clear(c.current) assert c.done() @@ -696,6 +697,7 @@ class TestFlowMaster: fm = flow.FlowMaster(DummyServer(ProxyConfig()), s) assert not fm.start_server_playback(pb, False, [], False, False, None, False) assert not fm.start_client_playback(pb, False) + fm.client_playback.testing = True q = Queue.Queue() assert not fm.state.flow_count() From be449b7129a55af81e249f992046d88a02efbc46 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 14 Nov 2014 16:13:45 +0100 Subject: [PATCH 50/89] fix #409 --- libmproxy/proxy/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 613662c36..55e2b30e3 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -99,7 +99,6 @@ class ConnectionHandler: # Delegate handling to the protocol handler protocol_handler(self.conntype)(self, **conn_kwargs).handle_messages() - self.del_server_connection() self.log("clientdisconnect", "info") self.channel.tell("clientdisconnect", self) @@ -112,6 +111,10 @@ class ConnectionHandler: print >> sys.stderr, traceback.format_exc() print >> sys.stderr, "mitmproxy has crashed!" print >> sys.stderr, "Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy" + finally: + # Make sure that we close the server connection in any case. + # The client connection is closed by the ProxyServer and does not have be handled here. + self.del_server_connection() def del_server_connection(self): """ @@ -119,6 +122,7 @@ class ConnectionHandler: """ if self.server_conn and self.server_conn.connection: self.server_conn.finish() + self.server_conn.close() self.log("serverdisconnect", "debug", ["%s:%s" % (self.server_conn.address.host, self.server_conn.address.port)]) self.channel.tell("serverdisconnect", self) From afc6ef99eac9460fa2c6fb63d6a3ba528d69f443 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 14 Nov 2014 16:18:05 +0100 Subject: [PATCH 51/89] bump version --- libmproxy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmproxy/version.py b/libmproxy/version.py index f4c0cd94e..8dcaecc80 100644 --- a/libmproxy/version.py +++ b/libmproxy/version.py @@ -1,4 +1,4 @@ -IVERSION = (0, 11) +IVERSION = (0, 11, 1) VERSION = ".".join(str(i) for i in IVERSION) MINORVERSION = ".".join(str(i) for i in IVERSION[:2]) NAME = "mitmproxy" From 4c2e87638a4e93e7d434a8d1846cbb455a7b35b6 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 12:49:21 +1300 Subject: [PATCH 52/89] Changelog, plus fix date of previous release --- CHANGELOG | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2df1c839d..86c41b13a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ -23 October 2014: mitmproxy 0.11: +15 November 2014: mitmproxy 0.11.1: + + * Bug fixes: connection leaks some crashes + + +7 November 2014: mitmproxy 0.11: * Performance improvements for mitmproxy console From c7a96b2fb121ba811aa5dbc580cb44fc6c4d2ed6 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 15 Nov 2014 00:52:26 +0100 Subject: [PATCH 53/89] always show error messages --- libmproxy/console/__init__.py | 2 +- libmproxy/dump.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index fc6600c1e..e6bc9b41c 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -1050,7 +1050,7 @@ class ConsoleMaster(flow.FlowMaster): self.eventlist[:] = [] def add_event(self, e, level="info"): - needed = dict(error=1, info=1, debug=2).get(level, 1) + needed = dict(error=0, info=1, debug=2).get(level, 1) if self.options.verbosity < needed: return diff --git a/libmproxy/dump.py b/libmproxy/dump.py index 4d899fe8a..0d9432c9c 100644 --- a/libmproxy/dump.py +++ b/libmproxy/dump.py @@ -155,7 +155,7 @@ class DumpMaster(flow.FlowMaster): return flows def add_event(self, e, level="info"): - needed = dict(error=1, info=1, debug=2).get(level, 1) + needed = dict(error=0, info=1, debug=2).get(level, 1) if self.o.verbosity >= needed: print >> self.outfile, e self.outfile.flush() From acce67e1fd468bf1ac4a536d007ac56d9ab652e3 Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 03:34:39 +0100 Subject: [PATCH 54/89] Initial checkin with har_extractor script. --- examples/har_extractor.py | 207 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 examples/har_extractor.py diff --git a/examples/har_extractor.py b/examples/har_extractor.py new file mode 100644 index 000000000..504e98df1 --- /dev/null +++ b/examples/har_extractor.py @@ -0,0 +1,207 @@ +""" + This inline script utilizes harparser.HAR from https://github.com/JustusW/harparser + to generate a HAR log object. +""" +from harparser import HAR +from datetime import datetime, timedelta, tzinfo + + +class UTC(tzinfo): + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + def tzname(self, dt): + return "Z" + + +class _HARLog(HAR.log): + def __init__(self): + HAR.log.__init__(self, {"version": "1.2", + "creator": {"name": "MITMPROXY HARExtractor", + "version": "0.1", + "comment": ""}, + "pages": [], + "entries": []}) + + def reset(self): + self.__init__() + + def add(self, obj): + if isinstance(obj, HAR.pages): + self['pages'].append(obj) + if isinstance(obj, HAR.entries): + self['entries'].append(obj) + + +def start(context, argv): + HARLog.reset() + + +def clientconnect(context, conn_handler): + """ + Called when a client initiates a connection to the proxy. Note that a + connection can correspond to multiple HTTP requests + """ + import time + context.log("clientconnect" + str(time.time())) + + +def serverconnect(context, conn_handler): + """ + Called when the proxy initiates a connection to the target server. Note that a + connection can correspond to multiple HTTP requests + """ + CONNECT_TIMES.pop(conn_handler.server_conn.address.address, None) + SSL_TIMES.pop(conn_handler.server_conn.address.address, None) + import time + context.log("serverconnect " + str(time.time())) + + +def request(context, flow): + """ + Called when a client request has been received. + """ + # print_attributes(flow) + # print_attributes(context) + import time + context.log("request " + str(time.time()) + " " + str(flow.request.timestamp_start)) + + +def responseheaders(context, flow): + """ + Called when the response headers for a server response have been received, + but the response body has not been processed yet. Can be used to tell mitmproxy + to stream the response. + """ + context.log("responseheaders") + + +def response(context, flow): + """ + Called when a server response has been received. + """ + import time + context.log("response " + str(time.time()) + " " + str(flow.request.timestamp_start)) + context.log("response " + str(time.time()) + " " + str(flow.response.timestamp_end)) + connect_time = CONNECT_TIMES.get(flow.server_conn.address.address, + int((flow.server_conn.timestamp_tcp_setup + - flow.server_conn.timestamp_start) + * 1000)) + CONNECT_TIMES[flow.server_conn.address.address] = -1 + + ssl_time = -1 + if flow.server_conn.timestamp_ssl_setup is not None: + ssl_time = SSL_TIMES.get(flow.server_conn.address.address, + int((flow.server_conn.timestamp_ssl_setup + - flow.server_conn.timestamp_tcp_setup) + * 1000)) + SSL_TIMES[flow.server_conn.address.address] = -1 + + timings = {'send': int((flow.request.timestamp_end - flow.request.timestamp_start) * 1000), + 'wait': int((flow.response.timestamp_start - flow.request.timestamp_end) * 1000), + 'receive': int((flow.response.timestamp_end - flow.response.timestamp_start) * 1000), + 'connect': connect_time, + 'ssl': ssl_time} + + full_time = 0 + for item in timings.values(): + if item > -1: + full_time += item + + entry = HAR.entries({"startedDateTime": datetime.fromtimestamp(flow.request.timestamp_start, tz=UTC()).isoformat(), + "time": full_time, + "request": {"method": flow.request.method, + "url": flow.request.url, + "httpVersion": ".".join([str(v) for v in flow.request.httpversion]), + "cookies": [{"name": k.strip(), "value": v[0]} + for k, v in (flow.request.get_cookies() or {}).iteritems()], + "headers": [{"name": k, "value": v} + for k, v in flow.request.headers], + "queryString": [{"name": k, "value": v} + for k, v in flow.request.get_query()], + "headersSize": len(str(flow.request.headers).split("\r\n\r\n")[0]), + "bodySize": len(flow.request.content), }, + "response": {"status": flow.response.code, + "statusText": flow.response.msg, + "httpVersion": ".".join([str(v) for v in flow.response.httpversion]), + "cookies": [{"name": k.strip(), "value": v[0]} + for k, v in (flow.response.get_cookies() or {}).iteritems()], + "headers": [{"name": k, "value": v} + for k, v in flow.response.headers], + "content": {"size": len(flow.response.content), + "compression": len(flow.response.get_decoded_content()) - len( + flow.response.content), + "mimeType": flow.response.headers.get('Content-Type', ('', ))[0]}, + "redirectURL": flow.response.headers.get('Location', ''), + "headersSize": len(str(flow.response.headers).split("\r\n\r\n")[0]), + "bodySize": len(flow.response.content), }, + "cache": {}, + "timings": timings, }) + + if flow.request.url in HARPAGE_LIST or flow.request.headers.get('Referer', None) is None: + PAGE_COUNT[1] += 1 + page_id = "_".join([str(v) for v in PAGE_COUNT]) + HARLog.add(HAR.pages({"startedDateTime": entry['startedDateTime'], + "id": page_id, + "title": flow.request.url, })) + PAGE_REF[flow.request.url] = page_id + entry['pageref'] = page_id + + if flow.request.headers.get('Referer', (None, ))[0] in PAGE_REF.keys(): + entry['pageref'] = PAGE_REF[flow.request.headers['Referer'][0]] + PAGE_REF[flow.request.url] = entry['pageref'] + + HARLog.add(entry) + + +def error(context, flow): + """ + Called when a flow error has occured, e.g. invalid server responses, or + interrupted connections. This is distinct from a valid server HTTP error + response, which is simply a response with an HTTP error code. + """ + # context.log("error") + + +def clientdisconnect(context, conn_handler): + """ + Called when a client disconnects from the proxy. + """ + # print "clientdisconnect" + # print_attributes(context._master) + # print_attributes(conn_handler) + + +def done(context): + """ + Called once on script shutdown, after any other events. + """ + from pprint import pprint + import json + + pprint(json.loads(HARLog.json())) + print HARLog.json() + print HARLog.compress() + print "%s%%" % str(100. * len(HARLog.compress()) / len(HARLog.json())) + + +def print_attributes(obj, filter=None): + for attr in dir(obj): + # if "__" in attr: + # continue + if filter is not None and filter not in attr: + continue + value = getattr(obj, attr) + print "%s.%s" % ('obj', attr), value, type(value) + + +HARPAGE_LIST = ['https://github.com/'] +HARLog = _HARLog() + +CONNECT_TIMES = {} +SSL_TIMES = {} +PAGE_REF = {} +PAGE_COUNT = ['autopage', 0] From 645a4a0c044a5f18f4ee03cf76ad097590e6ba2c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 15:47:00 +1300 Subject: [PATCH 55/89] Some additions to the release checklist --- release/release-checklist | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/release/release-checklist b/release/release-checklist index 4ec74e7a9..c0d8d82d1 100644 --- a/release/release-checklist +++ b/release/release-checklist @@ -26,5 +26,20 @@ - Build the OSX binaries - Follow instructions in osxbinaries + - Package: + cp -r ./doc /tmp/osx-mitmproxy/ + mv /tmp/osx-mitmproxy /tmp/osx-mitmproxy-VERSION + tar -czvf /tmp/osx-mitmproxy-VERSION.tar.gz /tmp/osx-mitmproxy-VERSION + mv /tmp/osx-mitmproxy-VERSION.tar.gz ~/mitmproxy/www.mitmproxy.org/src/download + +- Build the sources for each project: + python ./setup.py sdist + mv ./dist/FILE ~/mitmproxy/www.mitmproxy.org/src/download +- Adjust links on www.mitmproxy.org + +- Upload to pypi for each project: + + python ./setup.py sdist upload + From 24c4df07e39d537a631c111df2eef36e8cb1bd70 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 16:14:08 +1300 Subject: [PATCH 56/89] First-order integration of configargparser to add config file support --- libmproxy/cmdline.py | 109 ++++++++++++++++++++++++---------- libmproxy/main.py | 77 +++++++++++++++++------- libmproxy/onboarding/app.py | 4 +- libmproxy/platform/windows.py | 4 +- libmproxy/proxy/config.py | 35 +++++++---- setup.py | 7 ++- 6 files changed, 165 insertions(+), 71 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 4a3b5a48d..09e25ada2 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -1,6 +1,6 @@ from __future__ import absolute_import import re -from argparse import ArgumentTypeError +from configargparse import ArgumentTypeError from netlib import http from . import filt, utils from .proxy import config @@ -22,7 +22,9 @@ def _parse_hook(s): elif len(parts) == 3: patt, a, b = parts else: - raise ParseException("Malformed hook specifier - too few clauses: %s" % s) + raise ParseException( + "Malformed hook specifier - too few clauses: %s" % s + ) if not a: raise ParseException("Empty clause: %s" % str(patt)) @@ -184,12 +186,16 @@ def common_options(parser): parser.add_argument( "--anticache", action="store_true", dest="anticache", default=False, - help="Strip out request headers that might cause the server to return 304-not-modified." + + help=""" + Strip out request headers that might cause the server to return + 304-not-modified. + """ ) parser.add_argument( - "--confdir", - action="store", type=str, dest="confdir", default='~/.mitmproxy', - help="Configuration directory, contains default CA file. (~/.mitmproxy)" + "--cadir", + action="store", type=str, dest="cadir", default=config.CA_DIR, + help="Location of the default mitmproxy CA files. (%s)"%config.CA_DIR ) parser.add_argument( "--host", @@ -210,11 +216,17 @@ def common_options(parser): "-s", action="append", type=str, dest="scripts", default=[], metavar='"script.py --bar"', - help="Run a script. Surround with quotes to pass script arguments. Can be passed multiple times." + help=""" + Run a script. Surround with quotes to pass script arguments. Can be + passed multiple times. + """ ) parser.add_argument( "-t", - action="store", dest="stickycookie_filt", default=None, metavar="FILTER", + action="store", + dest="stickycookie_filt", + default=None, + metavar="FILTER", help="Set sticky cookie filter. Matched against requests." ) parser.add_argument( @@ -241,7 +253,7 @@ def common_options(parser): "-Z", action="store", dest="body_size_limit", default=None, metavar="SIZE", - help="Byte size limit of HTTP request and response bodies." \ + help="Byte size limit of HTTP request and response bodies." " Understands k/m/g suffixes, i.e. 3m for 3 megabytes." ) parser.add_argument( @@ -249,9 +261,9 @@ def common_options(parser): action="store", dest="stream_large_bodies", default=None, metavar="SIZE", help=""" - Stream data to the client if response body exceeds the given threshold. - If streamed, the body will not be stored in any way. Understands k/m/g - suffixes, i.e. 3m for 3 megabytes. + Stream data to the client if response body exceeds the given + threshold. If streamed, the body will not be stored in any way. + Understands k/m/g suffixes, i.e. 3m for 3 megabytes. """ ) @@ -282,8 +294,11 @@ def common_options(parser): "--tcp", action="append", type=str, dest="tcp_hosts", default=[], metavar="HOST", - help="Generic TCP SSL proxy mode for all hosts that match the pattern. Similar to --ignore," - "but SSL connections are intercepted. The communication contents are printed to the event log in verbose mode." + help=""" + Generic TCP SSL proxy mode for all hosts that match the pattern. + Similar to --ignore, but SSL connections are intercepted. The + communication contents are printed to the event log in verbose mode. + """ ) group.add_argument( "-n", @@ -297,8 +312,14 @@ def common_options(parser): ) group.add_argument( "-R", - action="store", type=parse_server_spec, dest="reverse_proxy", default=None, - help="Forward all requests to upstream HTTP server: http[s][2http[s]]://host[:port]" + action="store", + type=parse_server_spec, + dest="reverse_proxy", + default=None, + help=""" + Forward all requests to upstream HTTP server: + http[s][2http[s]]://host[:port] + """ ) group.add_argument( "--socks", @@ -312,16 +333,20 @@ def common_options(parser): ) group.add_argument( "-U", - action="store", type=parse_server_spec, dest="upstream_proxy", default=None, + action="store", + type=parse_server_spec, + dest="upstream_proxy", + default=None, help="Forward all requests to upstream proxy server: http://host[:port]" ) group = parser.add_argument_group( "Advanced Proxy Options", """ - The following options allow a custom adjustment of the proxy behavior. - Normally, you don't want to use these options directly and use the provided wrappers instead (-R, -U, -T). - """.strip() + The following options allow a custom adjustment of the proxy + behavior. Normally, you don't want to use these options directly and + use the provided wrappers instead (-R, -U, -T). + """ ) group.add_argument( "--http-form-in", dest="http_form_in", default=None, @@ -343,13 +368,19 @@ def common_options(parser): group.add_argument( "--app-host", action="store", dest="app_host", default=APP_HOST, metavar="host", - help="Domain to serve the onboarding app from. For transparent mode, use an IP when\ - a DNS entry for the app domain is not present. Default: %s" % APP_HOST - + help=""" + Domain to serve the onboarding 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", - action="store", dest="app_port", default=APP_PORT, type=int, metavar="80", + action="store", + dest="app_port", + default=APP_PORT, + type=int, + metavar="80", help="Port to serve the onboarding app from." ) @@ -380,8 +411,10 @@ def common_options(parser): group.add_argument( "--norefresh", action="store_true", dest="norefresh", default=False, - help="Disable response refresh, " - "which updates times in cookies and headers for replayed responses." + help=""" + Disable response refresh, which updates times in cookies and headers + for replayed responses. + """ ) group.add_argument( "--no-pop", @@ -392,13 +425,17 @@ def common_options(parser): group.add_argument( "--replay-ignore-content", action="store_true", dest="replay_ignore_content", default=False, - help="Ignore request's content while searching for a saved flow to replay" + help=""" + Ignore request's content while searching for a saved flow to replay + """ ) group.add_argument( "--replay-ignore-param", action="append", dest="replay_ignore_params", type=str, - help="Request's parameters to be ignored while searching for a saved flow to replay" - "Can be passed multiple times." + help=""" + Request's parameters to be ignored while searching for a saved flow + to replay. Can be passed multiple times. + """ ) group = parser.add_argument_group( @@ -417,9 +454,12 @@ def common_options(parser): ) group.add_argument( "--replace-from-file", - action="append", type=str, dest="replace_file", default=[], - metavar="PATH", - help="Replacement pattern, where the replacement clause is a path to a file." + action = "append", type=str, dest="replace_file", default=[], + metavar = "PATH", + help = """ + Replacement pattern, where the replacement clause is a path to a + file. + """ ) group = parser.add_argument_group( @@ -455,7 +495,10 @@ def common_options(parser): "--singleuser", action="store", dest="auth_singleuser", type=str, metavar="USER", - help="Allows access to a a single user, specified in the form username:password." + help=""" + Allows access to a a single user, specified in the form + username:password. + """ ) user_specification_group.add_argument( "--htpasswd", diff --git a/libmproxy/main.py b/libmproxy/main.py index 2d6a01196..9cad5dcc2 100644 --- a/libmproxy/main.py +++ b/libmproxy/main.py @@ -1,4 +1,5 @@ from __future__ import print_function, absolute_import +import configargparse import argparse import os import signal @@ -11,25 +12,38 @@ from .proxy.server import DummyServer, ProxyServer def check_versions(): """ - Having installed a wrong version of pyOpenSSL or netlib is unfortunately a very common source of error. - Check before every start that both versions are somewhat okay. + Having installed a wrong version of pyOpenSSL or netlib is unfortunately a + very common source of error. Check before every start that both versions are + somewhat okay. """ - # We don't introduce backward-incompatible changes in patch versions. Only consider major and minor version. + # We don't introduce backward-incompatible changes in patch versions. Only + # consider major and minor version. if netlib.version.IVERSION[:2] != version.IVERSION[:2]: print( "Warning: You are using mitmdump %s with netlib %s. " - "Most likely, that doesn't work - please upgrade!" % (version.VERSION, netlib.version.VERSION), - file=sys.stderr) - import OpenSSL, inspect - + "Most likely, that won't work - please upgrade!" % ( + version.VERSION, netlib.version.VERSION + ), + file=sys.stderr + ) + import OpenSSL + import inspect v = tuple([int(x) for x in OpenSSL.__version__.split(".")][:2]) if v < (0, 14): - print("You are using an outdated version of pyOpenSSL: mitmproxy requires pyOpenSSL 0.14 or greater.", - file=sys.stderr) - # Some users apparently have multiple versions of pyOpenSSL installed. Report which one we got. + print( + "You are using an outdated version of pyOpenSSL:" + " mitmproxy requires pyOpenSSL 0.14 or greater.", + file=sys.stderr + ) + # Some users apparently have multiple versions of pyOpenSSL installed. + # Report which one we got. pyopenssl_path = os.path.dirname(inspect.getfile(OpenSSL)) - print("Your pyOpenSSL %s installation is located at %s" % (OpenSSL.__version__, pyopenssl_path), - file=sys.stderr) + print( + "Your pyOpenSSL %s installation is located at %s" % ( + OpenSSL.__version__, pyopenssl_path + ), + file=sys.stderr + ) sys.exit(1) @@ -38,8 +52,14 @@ def assert_utf8_env(): for i in ["LANG", "LC_CTYPE", "LC_ALL"]: spec += os.environ.get(i, "").lower() if "utf" not in spec: - print("Error: mitmproxy requires a UTF console environment.", file=sys.stderr) - print("Set your LANG enviroment variable to something like en_US.UTF-8", file=sys.stderr) + print( + "Error: mitmproxy requires a UTF console environment.", + file=sys.stderr + ) + print( + "Set your LANG enviroment variable to something like en_US.UTF-8", + file=sys.stderr + ) sys.exit(1) @@ -55,12 +75,17 @@ def get_server(dummy_server, options): def mitmproxy_cmdline(): - # Don't import libmproxy.console for mitmdump, urwid is not available on all platforms. + # Don't import libmproxy.console for mitmdump, urwid is not available on all + # platforms. from . import console from .console import palettes - parser = argparse.ArgumentParser(usage="%(prog)s [options]") - parser.add_argument('--version', action='version', version=version.NAMEVERSION) + parser = configargparse.ArgumentParser(usage="%(prog)s [options]") + parser.add_argument( + '--version', + action='version', + version=version.NAMEVERSION + ) cmdline.common_options(parser) parser.add_argument( "--palette", type=str, default="dark", @@ -113,13 +138,21 @@ def mitmproxy(): # pragma: nocover def mitmdump_cmdline(): from . import dump - parser = argparse.ArgumentParser(usage="%(prog)s [options] [filter]") - parser.add_argument('--version', action='version', version="mitmdump" + " " + version.VERSION) + parser = configargparse.ArgumentParser(usage="%(prog)s [options] [filter]") + + parser.add_argument( + '--version', + action= 'version', + version= "mitmdump" + " " + version.VERSION + ) cmdline.common_options(parser) parser.add_argument( "--keepserving", - action="store_true", dest="keepserving", default=False, - help="Continue serving after client playback or file read. We exit by default." + action= "store_true", dest="keepserving", default=False, + help= """ + Continue serving after client playback or file read. We exit by + default. + """ ) parser.add_argument( "-d", @@ -166,7 +199,7 @@ def mitmdump(): # pragma: nocover def mitmweb_cmdline(): from . import web - parser = argparse.ArgumentParser(usage="%(prog)s [options]") + parser = configargparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument( '--version', action='version', diff --git a/libmproxy/onboarding/app.py b/libmproxy/onboarding/app.py index 9b5db38a2..4023fae2e 100644 --- a/libmproxy/onboarding/app.py +++ b/libmproxy/onboarding/app.py @@ -18,12 +18,12 @@ def index(): @mapp.route("/cert/pem") def certs_pem(): - p = os.path.join(master().server.config.confdir, config.CONF_BASENAME + "-ca-cert.pem") + p = os.path.join(master().server.config.cadir, config.CONF_BASENAME + "-ca-cert.pem") return flask.Response(open(p, "rb").read(), mimetype='application/x-x509-ca-cert') @mapp.route("/cert/p12") def certs_p12(): - p = os.path.join(master().server.config.confdir, config.CONF_BASENAME + "-ca-cert.p12") + p = os.path.join(master().server.config.cadir, config.CONF_BASENAME + "-ca-cert.p12") return flask.Response(open(p, "rb").read(), mimetype='application/x-pkcs12') diff --git a/libmproxy/platform/windows.py b/libmproxy/platform/windows.py index ddbbed529..066a377d9 100644 --- a/libmproxy/platform/windows.py +++ b/libmproxy/platform/windows.py @@ -1,4 +1,4 @@ -import argparse +import configargparse import cPickle as pickle from ctypes import byref, windll, Structure from ctypes.wintypes import DWORD @@ -361,7 +361,7 @@ class TransparentProxy(object): if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Windows Transparent Proxy") + parser = configargparse.ArgumentParser(description="Windows Transparent Proxy") parser.add_argument('--mode', choices=['forward', 'local', 'both'], default="both", help='redirection operation mode: "forward" to only redirect forwarded packets, ' '"local" to only redirect packets originating from the local machine') diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index a228192a4..3d373a28b 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -7,7 +7,7 @@ from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMod TRANSPARENT_SSL_PORTS = [443, 8443] CONF_BASENAME = "mitmproxy" -CONF_DIR = "~/.mitmproxy" +CA_DIR = "~/.mitmproxy" class HostMatcher(object): @@ -28,12 +28,27 @@ class HostMatcher(object): class ProxyConfig: - def __init__(self, host='', port=8080, server_version=version.NAMEVERSION, - confdir=CONF_DIR, clientcerts=None, - no_upstream_cert=False, body_size_limit=None, - mode=None, upstream_server=None, http_form_in=None, http_form_out=None, - authenticator=None, ignore_hosts=[], tcp_hosts=[], - ciphers=None, certs=[], certforward=False, ssl_ports=TRANSPARENT_SSL_PORTS): + def __init__( + self, + host='', + port=8080, + server_version=version.NAMEVERSION, + cadir=CA_DIR, + clientcerts=None, + no_upstream_cert=False, + body_size_limit=None, + mode=None, + upstream_server=None, + http_form_in=None, + http_form_out=None, + authenticator=None, + ignore_hosts=[], + tcp_hosts=[], + ciphers=None, + certs=[], + certforward=False, + ssl_ports=TRANSPARENT_SSL_PORTS + ): self.host = host self.port = port self.server_version = server_version @@ -60,8 +75,8 @@ class ProxyConfig: self.check_ignore = HostMatcher(ignore_hosts) self.check_tcp = HostMatcher(tcp_hosts) self.authenticator = authenticator - self.confdir = os.path.expanduser(confdir) - self.certstore = certutils.CertStore.from_store(self.confdir, CONF_BASENAME) + self.cadir = os.path.expanduser(cadir) + self.certstore = certutils.CertStore.from_store(self.cadir, CONF_BASENAME) for spec, cert in certs: self.certstore.add_cert_file(spec, cert) self.certforward = certforward @@ -136,7 +151,7 @@ def process_proxy_options(parser, options): return ProxyConfig( host=options.addr, port=options.port, - confdir=options.confdir, + cadir=options.cadir, clientcerts=options.clientcerts, no_upstream_cert=options.no_upstream_cert, body_size_limit=body_size_limit, diff --git a/setup.py b/setup.py index 08ccbbfd4..55c2a87e8 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,8 @@ deps = { "pyOpenSSL>=0.14", "Flask>=0.10.1", "tornado>=4.0.2", - "sortedcontainers>=0.9.1" + "sortedcontainers>=0.9.1", + "configargparse>=0.9.3" } script_deps = { "mitmproxy": { @@ -72,7 +73,9 @@ setup( "nose>=1.3.0", "nose-cov>=1.6", "coveralls>=0.4.1", - "pathod>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION) + "pathod>=%s, <%s" % ( + version.MINORVERSION, version.NEXT_MINORVERSION + ) ], 'contentviews': [ "pyamf>=0.6.1", From 0906ee94acc2c25d4cb836461263d7a2d35b8b4a Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 16:14:59 +1300 Subject: [PATCH 57/89] Remove sortedcontainers from deps - we don't use it --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 55c2a87e8..5674a0370 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ deps = { "pyOpenSSL>=0.14", "Flask>=0.10.1", "tornado>=4.0.2", - "sortedcontainers>=0.9.1", "configargparse>=0.9.3" } script_deps = { From 9c88622e25033cab300d2bbde2811c346c3caa8c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 16:17:05 +1300 Subject: [PATCH 58/89] Adjust tests --- test/test_proxy.py | 12 ++++++------ test/tservers.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/test_proxy.py b/test/test_proxy.py index c396183b0..641b4f471 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -70,9 +70,9 @@ class TestProcessProxyOptions: def test_simple(self): assert self.p() - def test_confdir(self): - with tutils.tmpdir() as confdir: - self.assert_noerr("--confdir", confdir) + def test_cadir(self): + with tutils.tmpdir() as cadir: + self.assert_noerr("--cadir", cadir) @mock.patch("libmproxy.platform.resolver", None) def test_no_transparent(self): @@ -94,12 +94,12 @@ class TestProcessProxyOptions: self.assert_err("mutually exclusive", "-R", "http://localhost", "-T") def test_client_certs(self): - with tutils.tmpdir() as confdir: - self.assert_noerr("--client-certs", confdir) + with tutils.tmpdir() as cadir: + self.assert_noerr("--client-certs", cadir) self.assert_err("directory does not exist", "--client-certs", "nonexistent") def test_certs(self): - with tutils.tmpdir() as confdir: + with tutils.tmpdir() as cadir: self.assert_noerr("--cert", tutils.test_data.path("data/testkey.pem")) self.assert_err("does not exist", "--cert", "nonexistent") diff --git a/test/tservers.py b/test/tservers.py index 93c8a80a0..12154ba7d 100644 --- a/test/tservers.py +++ b/test/tservers.py @@ -99,7 +99,7 @@ class ProxTestBase(object): @classmethod def teardownAll(cls): - shutil.rmtree(cls.confdir) + shutil.rmtree(cls.cadir) cls.proxy.shutdown() cls.server.shutdown() cls.server2.shutdown() @@ -116,10 +116,10 @@ class ProxTestBase(object): @classmethod def get_proxy_config(cls): - cls.confdir = os.path.join(tempfile.gettempdir(), "mitmproxy") + cls.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy") return dict( no_upstream_cert = cls.no_upstream_cert, - confdir = cls.confdir, + cadir = cls.cadir, authenticator = cls.authenticator, certforward = cls.certforward, ssl_ports=([cls.server.port, cls.server2.port] if cls.ssl else []), From 6c1dc4522d7bf83c7b6c289f11f5a33d5b9a018f Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 16:29:38 +1300 Subject: [PATCH 59/89] Refactor command-line argument definition - Argument definitions live in cmdline.py - Parsing and initial processing lives in main.py --- libmproxy/cmdline.py | 121 ++++++++++++++++++++++++++++++++++++++--- libmproxy/main.py | 125 ++++--------------------------------------- 2 files changed, 124 insertions(+), 122 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 09e25ada2..99e977d45 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -1,8 +1,9 @@ from __future__ import absolute_import import re -from configargparse import ArgumentTypeError +import configargparse +import argparse from netlib import http -from . import filt, utils +from . import filt, utils, version from .proxy import config APP_HOST = "mitm.it" @@ -103,7 +104,9 @@ def parse_server_spec(url): p = http.parse_url(normalized_url) if not p or not p[1]: - raise ArgumentTypeError("Invalid server specification: %s" % url) + raise argparse.ArgumentTypeError( + "Invalid server specification: %s" % url + ) if url.lower().startswith("https2http"): ssl = [True, False] @@ -132,17 +135,19 @@ def get_common_options(options): try: p = parse_replace_hook(i) except ParseException, e: - raise ArgumentTypeError(e.message) + raise argparse.ArgumentTypeError(e.message) reps.append(p) for i in options.replace_file: try: patt, rex, path = parse_replace_hook(i) except ParseException, e: - raise ArgumentTypeError(e.message) + raise argparse.ArgumentTypeError(e.message) try: v = open(path, "rb").read() except IOError, e: - raise ArgumentTypeError("Could not read replace file: %s" % path) + raise argparse.ArgumentTypeError( + "Could not read replace file: %s" % path + ) reps.append((patt, rex, v)) setheaders = [] @@ -150,7 +155,7 @@ def get_common_options(options): try: p = parse_setheader(i) except ParseException, e: - raise ArgumentTypeError(e.message) + raise argparse.ArgumentTypeError(e.message) setheaders.append(p) return dict( @@ -508,3 +513,105 @@ def common_options(parser): ) config.ssl_option_group(parser) + + +def mitmproxy(): + # Don't import libmproxy.console for mitmdump, urwid is not available on all + # platforms. + from .console import palettes + + parser = configargparse.ArgumentParser(usage="%(prog)s [options]") + parser.add_argument( + '--version', + action='version', + version=version.NAMEVERSION + ) + common_options(parser) + parser.add_argument( + "--palette", type=str, default="dark", + action="store", dest="palette", + help="Select color palette: " + ", ".join(palettes.palettes.keys()) + ) + parser.add_argument( + "-e", + action="store_true", dest="eventlog", + help="Show event log." + ) + group = parser.add_argument_group( + "Filters", + "See help in mitmproxy for filter expression syntax." + ) + group.add_argument( + "-i", "--intercept", action="store", + type=str, dest="intercept", default=None, + help="Intercept filter expression." + ) + + return parser + + +def mitmdump(): + parser = configargparse.ArgumentParser(usage="%(prog)s [options] [filter]") + + parser.add_argument( + '--version', + action= 'version', + version= "mitmdump" + " " + version.VERSION + ) + common_options(parser) + parser.add_argument( + "--keepserving", + action= "store_true", dest="keepserving", default=False, + help= """ + Continue serving after client playback or file read. We exit by + default. + """ + ) + parser.add_argument( + "-d", + action="count", dest="flow_detail", default=1, + help="Increase flow detail display level. Can be passed multiple times." + ) + parser.add_argument('args', nargs=argparse.REMAINDER) + return parser + + +def mitmweb(): + parser = configargparse.ArgumentParser(usage="%(prog)s [options]") + parser.add_argument( + '--version', + action='version', + version="mitmweb" + " " + version.VERSION + ) + + group = parser.add_argument_group("Mitmweb") + group.add_argument( + "--wport", + action="store", type=int, dest="wport", default=8081, + metavar="PORT", + help="Mitmweb port." + ) + group.add_argument( + "--wiface", + action="store", dest="wiface", default="127.0.0.1", + metavar="IFACE", + help="Mitmweb interface." + ) + group.add_argument( + "--wdebug", + action="store_true", dest="wdebug", + help="Turn on mitmweb debugging" + ) + + common_options(parser) + group = parser.add_argument_group( + "Filters", + "See help in mitmproxy for filter expression syntax." + ) + group.add_argument( + "-i", "--intercept", action="store", + type=str, dest="intercept", default=None, + help="Intercept filter expression." + ) + return parser + diff --git a/libmproxy/main.py b/libmproxy/main.py index 9cad5dcc2..ffa012d3f 100644 --- a/libmproxy/main.py +++ b/libmproxy/main.py @@ -1,6 +1,4 @@ from __future__ import print_function, absolute_import -import configargparse -import argparse import os import signal import sys @@ -74,39 +72,13 @@ def get_server(dummy_server, options): sys.exit(1) -def mitmproxy_cmdline(): - # Don't import libmproxy.console for mitmdump, urwid is not available on all - # platforms. +def mitmproxy(): # pragma: nocover from . import console - from .console import palettes - parser = configargparse.ArgumentParser(usage="%(prog)s [options]") - parser.add_argument( - '--version', - action='version', - version=version.NAMEVERSION - ) - cmdline.common_options(parser) - parser.add_argument( - "--palette", type=str, default="dark", - action="store", dest="palette", - help="Select color palette: " + ", ".join(palettes.palettes.keys()) - ) - parser.add_argument( - "-e", - action="store_true", dest="eventlog", - help="Show event log." - ) - group = parser.add_argument_group( - "Filters", - "See help in mitmproxy for filter expression syntax." - ) - group.add_argument( - "-i", "--intercept", action="store", - type=str, dest="intercept", default=None, - help="Intercept filter expression." - ) + check_versions() + assert_utf8_env() + parser = cmdline.mitmproxy() options = parser.parse_args() if options.quiet: options.verbose = 0 @@ -117,15 +89,6 @@ def mitmproxy_cmdline(): console_options.eventlog = options.eventlog console_options.intercept = options.intercept - return console_options, proxy_config - - -def mitmproxy(): # pragma: nocover - from . import console - - check_versions() - assert_utf8_env() - console_options, proxy_config = mitmproxy_cmdline() server = get_server(console_options.no_server, proxy_config) m = console.ConsoleMaster(server, console_options) @@ -135,32 +98,12 @@ def mitmproxy(): # pragma: nocover pass -def mitmdump_cmdline(): +def mitmdump(): # pragma: nocover from . import dump - parser = configargparse.ArgumentParser(usage="%(prog)s [options] [filter]") - - parser.add_argument( - '--version', - action= 'version', - version= "mitmdump" + " " + version.VERSION - ) - cmdline.common_options(parser) - parser.add_argument( - "--keepserving", - action= "store_true", dest="keepserving", default=False, - help= """ - Continue serving after client playback or file read. We exit by - default. - """ - ) - parser.add_argument( - "-d", - action="count", dest="flow_detail", default=1, - help="Increase flow detail display level. Can be passed multiple times." - ) - parser.add_argument('args', nargs=argparse.REMAINDER) + check_versions() + parser = cmdline.mitmdump() options = parser.parse_args() if options.quiet: options.verbose = 0 @@ -172,14 +115,6 @@ def mitmdump_cmdline(): dump_options.keepserving = options.keepserving dump_options.filtstr = " ".join(options.args) if options.args else None - return dump_options, proxy_config - - -def mitmdump(): # pragma: nocover - from . import dump - - check_versions() - dump_options, proxy_config = mitmdump_cmdline() server = get_server(dump_options.no_server, proxy_config) try: @@ -197,44 +132,11 @@ def mitmdump(): # pragma: nocover pass -def mitmweb_cmdline(): +def mitmweb(): # pragma: nocover from . import web - parser = configargparse.ArgumentParser(usage="%(prog)s [options]") - parser.add_argument( - '--version', - action='version', - version="mitmweb" + " " + version.VERSION - ) - group = parser.add_argument_group("Mitmweb") - group.add_argument( - "--wport", - action="store", type=int, dest="wport", default=8081, - metavar="PORT", - help="Mitmweb port." - ) - group.add_argument( - "--wiface", - action="store", dest="wiface", default="127.0.0.1", - metavar="IFACE", - help="Mitmweb interface." - ) - group.add_argument( - "--wdebug", - action="store_true", dest="wdebug", - help="Turn on mitmweb debugging" - ) - - cmdline.common_options(parser) - group = parser.add_argument_group( - "Filters", - "See help in mitmproxy for filter expression syntax." - ) - group.add_argument( - "-i", "--intercept", action="store", - type=str, dest="intercept", default=None, - help="Intercept filter expression." - ) + check_versions() + parser = cmdline.mitmweb() options = parser.parse_args() if options.quiet: @@ -246,14 +148,7 @@ def mitmweb_cmdline(): web_options.wdebug = options.wdebug web_options.wiface = options.wiface web_options.wport = options.wport - return web_options, proxy_config - -def mitmweb(): # pragma: nocover - from . import web - - check_versions() - web_options, proxy_config = mitmweb_cmdline() server = get_server(web_options.no_server, proxy_config) m = web.WebMaster(server, web_options) From 09c503563ad2e42812bf8043aedd9ecf980babf6 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 17:25:05 +1300 Subject: [PATCH 60/89] Enable config file parsing We support 4 different config files: ~/.mitmproxy/common.conf: Options that are common to all tools ~/.mitmproxy/mitmproxy.conf: Options for mitmproxy ~/.mitmproxy/mitmdump.conf: Options for mitmdump ~/.mitmproxy/mitmweb.conf: Options for mitmweb Options in the tool-specific config files over-ride options in common.conf. If a non-common option is put in common.conf, an error will be raised if a non-supporting tool is used. --- libmproxy/cmdline.py | 36 ++++++++++++++++++++++++++++++++---- test/test_cmdline.py | 18 +++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 99e977d45..20dd0b6ad 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +import os import re import configargparse import argparse @@ -311,7 +312,7 @@ def common_options(parser): help="Don't start a proxy server." ) group.add_argument( - "-p", + "-p", "--port", action="store", type=int, dest="port", default=8080, help="Proxy service port." ) @@ -520,7 +521,16 @@ def mitmproxy(): # platforms. from .console import palettes - parser = configargparse.ArgumentParser(usage="%(prog)s [options]") + parser = configargparse.ArgumentParser( + usage="%(prog)s [options]", + args_for_setting_config_path = ["--conf"], + default_config_files = [ + os.path.join(config.CA_DIR, "common.conf"), + os.path.join(config.CA_DIR, "mitmproxy.conf") + ], + add_config_file_help = True, + add_env_var_help = True + ) parser.add_argument( '--version', action='version', @@ -551,7 +561,16 @@ def mitmproxy(): def mitmdump(): - parser = configargparse.ArgumentParser(usage="%(prog)s [options] [filter]") + parser = configargparse.ArgumentParser( + usage="%(prog)s [options] [filter]", + args_for_setting_config_path = ["--conf"], + default_config_files = [ + os.path.join(config.CA_DIR, "common.conf"), + os.path.join(config.CA_DIR, "mitmdump.conf") + ], + add_config_file_help = True, + add_env_var_help = True + ) parser.add_argument( '--version', @@ -577,7 +596,16 @@ def mitmdump(): def mitmweb(): - parser = configargparse.ArgumentParser(usage="%(prog)s [options]") + parser = configargparse.ArgumentParser( + usage="%(prog)s [options]", + args_for_setting_config_path = ["--conf"], + default_config_files = [ + os.path.join(config.CA_DIR, "common.conf"), + os.path.join(config.CA_DIR, "mitmweb.conf") + ], + add_config_file_help = True, + add_env_var_help = True + ) parser.add_argument( '--version', action='version', diff --git a/test/test_cmdline.py b/test/test_cmdline.py index 12e8aa897..476fc6206 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -1,7 +1,6 @@ import argparse from libmproxy import cmdline import tutils -import os.path def test_parse_replace_hook(): @@ -51,6 +50,7 @@ def test_parse_setheaders(): x = cmdline.parse_setheader("/foo/bar/voing") assert x == ("foo", "bar", "voing") + def test_common(): parser = argparse.ArgumentParser() cmdline.common_options(parser) @@ -108,3 +108,19 @@ def test_common(): assert len(v) == 1 assert v[0][2].strip() == "replacecontents" + +def test_mitmproxy(): + ap = cmdline.mitmproxy() + assert ap + + +def test_mitmdump(): + ap = cmdline.mitmdump() + assert ap + + +def test_mitmweb(): + ap = cmdline.mitmweb() + assert ap + + From 7d76f3e992409caf1997b031a910b8bfdf3fc2a3 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 17:41:04 +1300 Subject: [PATCH 61/89] Make sure all command-line arguments have a long form ... so they can be used in config files --- libmproxy/cmdline.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 20dd0b6ad..27847e754 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -209,17 +209,17 @@ def common_options(parser): help="Use the Host header to construct URLs for display." ) parser.add_argument( - "-q", + "-q", "--quiet", action="store_true", dest="quiet", help="Quiet." ) parser.add_argument( - "-r", + "-r", "--read-flows", action="store", dest="rfile", default=None, help="Read flows from file." ) parser.add_argument( - "-s", + "-s", "--script", action="append", type=str, dest="scripts", default=[], metavar='"script.py --bar"', help=""" @@ -228,7 +228,7 @@ def common_options(parser): """ ) parser.add_argument( - "-t", + "-t", "--stickycookie", action="store", dest="stickycookie_filt", default=None, @@ -236,27 +236,27 @@ def common_options(parser): help="Set sticky cookie filter. Matched against requests." ) parser.add_argument( - "-u", + "-u", "--stickyauth", action="store", dest="stickyauth_filt", default=None, metavar="FILTER", help="Set sticky auth filter. Matched against requests." ) parser.add_argument( - "-v", + "-v", "--verbose", action="store_const", dest="verbose", default=1, const=2, help="Increase event log verbosity." ) parser.add_argument( - "-w", + "-w", "--wfile", action="store", dest="wfile", default=None, help="Write flows to file." ) parser.add_argument( - "-z", + "-z", "--anticomp", action="store_true", dest="anticomp", default=False, help="Try to convince servers to send us un-compressed data." ) parser.add_argument( - "-Z", + "-Z", "--body-size-limit", action="store", dest="body_size_limit", default=None, metavar="SIZE", help="Byte size limit of HTTP request and response bodies." @@ -279,7 +279,7 @@ def common_options(parser): # it's already in a different group. - our own error messages are more # helpful group.add_argument( - "-b", + "-b", "--bind-address", action="store", type=str, dest="addr", default='', help="Address to bind proxy to (defaults to all interfaces)" ) @@ -307,7 +307,7 @@ def common_options(parser): """ ) group.add_argument( - "-n", + "-n", "--no-server", action="store_true", dest="no_server", help="Don't start a proxy server." ) @@ -317,7 +317,7 @@ def common_options(parser): help="Proxy service port." ) group.add_argument( - "-R", + "-R", "--reverse", action="store", type=parse_server_spec, dest="reverse_proxy", @@ -333,12 +333,12 @@ def common_options(parser): help="Set SOCKS5 proxy mode." ) group.add_argument( - "-T", + "-T", "--transparent", action="store_true", dest="transparent_proxy", default=False, help="Set transparent proxy mode." ) group.add_argument( - "-U", + "-U", "--upstream", action="store", type=parse_server_spec, dest="upstream_proxy", @@ -367,7 +367,7 @@ def common_options(parser): group = parser.add_argument_group("Onboarding App") group.add_argument( - "-a", + "-a", "--noapp", action="store_false", dest="app", default=True, help="Disable the mitmproxy onboarding app." ) @@ -392,19 +392,19 @@ def common_options(parser): group = parser.add_argument_group("Client Replay") group.add_argument( - "-c", + "-c", "--client-replay", action="store", dest="client_replay", default=None, metavar="PATH", help="Replay client requests from a saved file." ) group = parser.add_argument_group("Server Replay") group.add_argument( - "-S", + "-S", "--server-replay", action="store", dest="server_replay", default=None, metavar="PATH", help="Replay server responses from a saved file." ) group.add_argument( - "-k", + "-k", "--kill", action="store_true", dest="kill", default=False, help="Kill extra requests during replay." ) @@ -543,7 +543,7 @@ def mitmproxy(): help="Select color palette: " + ", ".join(palettes.palettes.keys()) ) parser.add_argument( - "-e", + "-e", "--eventlog", action="store_true", dest="eventlog", help="Show event log." ) @@ -556,7 +556,6 @@ def mitmproxy(): type=str, dest="intercept", default=None, help="Intercept filter expression." ) - return parser @@ -587,7 +586,7 @@ def mitmdump(): """ ) parser.add_argument( - "-d", + "-d", "--detail", action="count", dest="flow_detail", default=1, help="Increase flow detail display level. Can be passed multiple times." ) From 5af7c9ebf4c61f2397ea18c132f4253362ca075d Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 17:47:39 +1300 Subject: [PATCH 62/89] Exclude main.py from coverage analysis --- .coveragerc | 2 +- libmproxy/main.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 7a4e3ab73..70ff48e78 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,5 +2,5 @@ branch = True [report] -omit = *contrib*, *tnetstring*, *platform*, *console* +omit = *contrib*, *tnetstring*, *platform*, *console*, *main.py include = *libmproxy* diff --git a/libmproxy/main.py b/libmproxy/main.py index ffa012d3f..e5b7f56b2 100644 --- a/libmproxy/main.py +++ b/libmproxy/main.py @@ -8,6 +8,9 @@ from .proxy import process_proxy_options, ProxyServerError from .proxy.server import DummyServer, ProxyServer +# This file is not included in coverage analysis or tests - anything that can be +# tested should live elsewhere. + def check_versions(): """ Having installed a wrong version of pyOpenSSL or netlib is unfortunately a From 23a4f159fd1cd529743fc445f3747062fc318534 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 17:51:21 +1300 Subject: [PATCH 63/89] Remove last vestiges of argparse --- libmproxy/cmdline.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 27847e754..83aac7906 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -2,7 +2,6 @@ from __future__ import absolute_import import os import re import configargparse -import argparse from netlib import http from . import filt, utils, version from .proxy import config @@ -105,7 +104,7 @@ def parse_server_spec(url): p = http.parse_url(normalized_url) if not p or not p[1]: - raise argparse.ArgumentTypeError( + raise configargparse.ArgumentTypeError( "Invalid server specification: %s" % url ) @@ -136,17 +135,17 @@ def get_common_options(options): try: p = parse_replace_hook(i) except ParseException, e: - raise argparse.ArgumentTypeError(e.message) + raise configargparse.ArgumentTypeError(e.message) reps.append(p) for i in options.replace_file: try: patt, rex, path = parse_replace_hook(i) except ParseException, e: - raise argparse.ArgumentTypeError(e.message) + raise configargparse.ArgumentTypeError(e.message) try: v = open(path, "rb").read() except IOError, e: - raise argparse.ArgumentTypeError( + raise configargparse.ArgumentTypeError( "Could not read replace file: %s" % path ) reps.append((patt, rex, v)) @@ -156,7 +155,7 @@ def get_common_options(options): try: p = parse_setheader(i) except ParseException, e: - raise argparse.ArgumentTypeError(e.message) + raise configargparse.ArgumentTypeError(e.message) setheaders.append(p) return dict( @@ -590,7 +589,7 @@ def mitmdump(): action="count", dest="flow_detail", default=1, help="Increase flow detail display level. Can be passed multiple times." ) - parser.add_argument('args', nargs=argparse.REMAINDER) + parser.add_argument('args', nargs="...") return parser From aa77a52a069e832236495f2aa7bfdbc90f26b59c Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 15 Nov 2014 17:58:38 +1300 Subject: [PATCH 64/89] One common --version flag --- libmproxy/cmdline.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py index 83aac7906..b892f1fd8 100644 --- a/libmproxy/cmdline.py +++ b/libmproxy/cmdline.py @@ -188,6 +188,11 @@ def get_common_options(options): def common_options(parser): + parser.add_argument( + '--version', + action= 'version', + version= "%(prog)s" + " " + version.VERSION + ) parser.add_argument( "--anticache", action="store_true", dest="anticache", default=False, @@ -530,11 +535,6 @@ def mitmproxy(): add_config_file_help = True, add_env_var_help = True ) - parser.add_argument( - '--version', - action='version', - version=version.NAMEVERSION - ) common_options(parser) parser.add_argument( "--palette", type=str, default="dark", @@ -570,11 +570,6 @@ def mitmdump(): add_env_var_help = True ) - parser.add_argument( - '--version', - action= 'version', - version= "mitmdump" + " " + version.VERSION - ) common_options(parser) parser.add_argument( "--keepserving", @@ -604,11 +599,6 @@ def mitmweb(): add_config_file_help = True, add_env_var_help = True ) - parser.add_argument( - '--version', - action='version', - version="mitmweb" + " " + version.VERSION - ) group = parser.add_argument_group("Mitmweb") group.add_argument( From fd48a70128581c508420901910c285f247c930c7 Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 18:38:59 +0100 Subject: [PATCH 65/89] Updated documentation and cleaned up the code. --- examples/har_extractor.py | 194 ++++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 104 deletions(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index 504e98df1..bc67d2993 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -37,110 +37,104 @@ class _HARLog(HAR.log): def start(context, argv): + """ + On start we reset the HAR, it's not really necessary since it will have been + instantiated earlier during initial parsing of this file. You will have to + adapt this to suit your actual needs of HAR generation. + """ HARLog.reset() - - -def clientconnect(context, conn_handler): - """ - Called when a client initiates a connection to the proxy. Note that a - connection can correspond to multiple HTTP requests - """ - import time - context.log("clientconnect" + str(time.time())) - - -def serverconnect(context, conn_handler): - """ - Called when the proxy initiates a connection to the target server. Note that a - connection can correspond to multiple HTTP requests - """ - CONNECT_TIMES.pop(conn_handler.server_conn.address.address, None) - SSL_TIMES.pop(conn_handler.server_conn.address.address, None) - import time - context.log("serverconnect " + str(time.time())) - - -def request(context, flow): - """ - Called when a client request has been received. - """ - # print_attributes(flow) - # print_attributes(context) - import time - context.log("request " + str(time.time()) + " " + str(flow.request.timestamp_start)) - - -def responseheaders(context, flow): - """ - Called when the response headers for a server response have been received, - but the response body has not been processed yet. Can be used to tell mitmproxy - to stream the response. - """ - context.log("responseheaders") + context.seen_server_connect = set() + context.seen_server_ssl = set() def response(context, flow): """ - Called when a server response has been received. + Called when a server response has been received. At the time of this message both + a request and a response are present and completely done. """ - import time - context.log("response " + str(time.time()) + " " + str(flow.request.timestamp_start)) - context.log("response " + str(time.time()) + " " + str(flow.response.timestamp_end)) - connect_time = CONNECT_TIMES.get(flow.server_conn.address.address, - int((flow.server_conn.timestamp_tcp_setup - - flow.server_conn.timestamp_start) - * 1000)) - CONNECT_TIMES[flow.server_conn.address.address] = -1 + connect_time = -1 + if flow.server_conn not in context.seen_server_connect: + # Calculate the connect_time for this server_conn. Afterwards add it to seen list, in + # order to avoid the connect_time being present in entries that use an existing connection. + connect_time = flow.server_conn.timestamp_tcp_setup - flow.server_conn.timestamp_start + context.seen_server_connect.add(flow.server_conn) ssl_time = -1 - if flow.server_conn.timestamp_ssl_setup is not None: - ssl_time = SSL_TIMES.get(flow.server_conn.address.address, - int((flow.server_conn.timestamp_ssl_setup - - flow.server_conn.timestamp_tcp_setup) - * 1000)) - SSL_TIMES[flow.server_conn.address.address] = -1 + if flow.server_conn not in context.seen_server_connect \ + and flow.server_conn.timestamp_ssl_setup is not None: + # Get the ssl_time for this server_conn as the difference between the start of the successful + # tcp setup and the successful ssl setup. Afterwards add it to seen list, in order to avoid + # the ssl_time being present in entries that use an existing connection. If no ssl setup has + # been made initiate it is also left as -1 since it doesn't apply to this connection. + ssl_time = flow.server_conn.timestamp_ssl_setup - flow.server_conn.timestamp_tcp_setup + context.seen_server_ssl.add(flow.server_conn) - timings = {'send': int((flow.request.timestamp_end - flow.request.timestamp_start) * 1000), - 'wait': int((flow.response.timestamp_start - flow.request.timestamp_end) * 1000), - 'receive': int((flow.response.timestamp_end - flow.response.timestamp_start) * 1000), - 'connect': connect_time, - 'ssl': ssl_time} + # Calculate the raw timings from the different timestamps present in the request and response object. + # For lack of a way to measure it dns timings can not be calculated. The same goes for HAR blocked: + # MITMProxy will open a server connection as soon as it receives the host and port from the client + # connection. So the time spent waiting is actually spent waiting between request.timestamp_end and + # response.timestamp_start thus it correlates to HAR wait instead. + timings_raw = {'send': flow.request.timestamp_end - flow.request.timestamp_start, + 'wait': flow.response.timestamp_start - flow.request.timestamp_end, + 'receive': flow.response.timestamp_end - flow.response.timestamp_start, + 'connect': connect_time, + 'ssl': ssl_time} + # HAR timings are integers in ms, so we have to re-encode the raw timings to that format. + timings = dict([(key, int(1000 * value)) for key, value in timings_raw.iteritems()]) + + # The full_time is the sum of all timings. Timings set to -1 will be ignored as per spec. full_time = 0 for item in timings.values(): if item > -1: full_time += item - entry = HAR.entries({"startedDateTime": datetime.fromtimestamp(flow.request.timestamp_start, tz=UTC()).isoformat(), + started_date_time = datetime.fromtimestamp(flow.request.timestamp_start, tz=UTC()).isoformat() + + request_query_string = [{"name": k, "value": v} for k, v in flow.request.get_query()] + request_http_version = ".".join([str(v) for v in flow.request.httpversion]) + # Cookies are shaped as tuples by MITMProxy. + request_cookies = [{"name": k.strip(), "value": v[0]} for k, v in (flow.request.get_cookies() or {}).iteritems()] + request_headers = [{"name": k, "value": v} for k, v in flow.request.headers] + request_headers_size = len(str(flow.request.headers)) + request_body_size = len(flow.request.content) + + response_http_version = ".".join([str(v) for v in flow.response.httpversion]) + # Cookies are shaped as tuples by MITMProxy. + response_cookies = [{"name": k.strip(), "value": v[0]} for k, v in (flow.response.get_cookies() or {}).iteritems()] + response_headers = [{"name": k, "value": v} for k, v in flow.response.headers] + response_headers_size = len(str(flow.response.headers)) + response_body_size = len(flow.response.content) + response_body_decoded_size = len(flow.response.content) + response_body_compression = response_body_decoded_size - response_body_size + response_mime_type = flow.response.headers.get('Content-Type', [''])[0] + response_redirect_url = flow.response.headers.get('Location', [''])[0] + + entry = HAR.entries({"startedDateTime": started_date_time, "time": full_time, "request": {"method": flow.request.method, "url": flow.request.url, - "httpVersion": ".".join([str(v) for v in flow.request.httpversion]), - "cookies": [{"name": k.strip(), "value": v[0]} - for k, v in (flow.request.get_cookies() or {}).iteritems()], - "headers": [{"name": k, "value": v} - for k, v in flow.request.headers], - "queryString": [{"name": k, "value": v} - for k, v in flow.request.get_query()], - "headersSize": len(str(flow.request.headers).split("\r\n\r\n")[0]), - "bodySize": len(flow.request.content), }, + "httpVersion": request_http_version, + "cookies": request_cookies, + "headers": request_headers, + "queryString": request_query_string, + "headersSize": request_headers_size, + "bodySize": request_body_size, }, "response": {"status": flow.response.code, "statusText": flow.response.msg, - "httpVersion": ".".join([str(v) for v in flow.response.httpversion]), - "cookies": [{"name": k.strip(), "value": v[0]} - for k, v in (flow.response.get_cookies() or {}).iteritems()], - "headers": [{"name": k, "value": v} - for k, v in flow.response.headers], - "content": {"size": len(flow.response.content), - "compression": len(flow.response.get_decoded_content()) - len( - flow.response.content), - "mimeType": flow.response.headers.get('Content-Type', ('', ))[0]}, - "redirectURL": flow.response.headers.get('Location', ''), - "headersSize": len(str(flow.response.headers).split("\r\n\r\n")[0]), - "bodySize": len(flow.response.content), }, + "httpVersion": response_http_version, + "cookies": response_cookies, + "headers": response_headers, + "content": {"size": response_body_size, + "compression": response_body_compression, + "mimeType": response_mime_type}, + "redirectURL": response_redirect_url, + "headersSize": response_headers_size, + "bodySize": response_body_size, }, "cache": {}, "timings": timings, }) + # If the current url is in HARPAGE_LIST or does not have a referer we add it as a new pages object. if flow.request.url in HARPAGE_LIST or flow.request.headers.get('Referer', None) is None: PAGE_COUNT[1] += 1 page_id = "_".join([str(v) for v in PAGE_COUNT]) @@ -150,31 +144,14 @@ def response(context, flow): PAGE_REF[flow.request.url] = page_id entry['pageref'] = page_id - if flow.request.headers.get('Referer', (None, ))[0] in PAGE_REF.keys(): + # Lookup the referer in our PAGE_REF dict to point this entries pageref attribute to the right pages object. + elif flow.request.headers.get('Referer', (None, ))[0] in PAGE_REF.keys(): entry['pageref'] = PAGE_REF[flow.request.headers['Referer'][0]] PAGE_REF[flow.request.url] = entry['pageref'] HARLog.add(entry) -def error(context, flow): - """ - Called when a flow error has occured, e.g. invalid server responses, or - interrupted connections. This is distinct from a valid server HTTP error - response, which is simply a response with an HTTP error code. - """ - # context.log("error") - - -def clientdisconnect(context, conn_handler): - """ - Called when a client disconnects from the proxy. - """ - # print "clientdisconnect" - # print_attributes(context._master) - # print_attributes(conn_handler) - - def done(context): """ Called once on script shutdown, after any other events. @@ -182,13 +159,21 @@ def done(context): from pprint import pprint import json - pprint(json.loads(HARLog.json())) - print HARLog.json() - print HARLog.compress() - print "%s%%" % str(100. * len(HARLog.compress()) / len(HARLog.json())) + json_dump = HARLog.json() + compressed_json_dump = HARLog.compress() + + print "=" * 100 + pprint(json.loads(json_dump)) + print "=" * 100 + print "HAR log finished with %s bytes (%s bytes compressed)" % (len(json_dump), len(compressed_json_dump)) + print "Compression rate is %s%%" % str(100. * len(compressed_json_dump) / len(json_dump)) + print "=" * 100 def print_attributes(obj, filter=None): + """ + Useful helper method to quickly get all attributes of an object and its values. + """ for attr in dir(obj): # if "__" in attr: # continue @@ -198,6 +183,7 @@ def print_attributes(obj, filter=None): print "%s.%s" % ('obj', attr), value, type(value) +# Some initializations. Add any page you want to have its own pages object to HARPAGE_LIST HARPAGE_LIST = ['https://github.com/'] HARLog = _HARLog() From f3a78d4795a97f99194a46c764cfeb1fe6fd01f2 Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 18:41:51 +0100 Subject: [PATCH 66/89] Improved helper method, marginally. --- examples/har_extractor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index bc67d2993..666dc03f0 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -170,14 +170,14 @@ def done(context): print "=" * 100 -def print_attributes(obj, filter=None): +def print_attributes(obj, filter_string=None, hide_privates=False): """ Useful helper method to quickly get all attributes of an object and its values. """ for attr in dir(obj): - # if "__" in attr: - # continue - if filter is not None and filter not in attr: + if hide_privates and "__" in attr: + continue + if filter_string is not None and filter_string not in attr: continue value = getattr(obj, attr) print "%s.%s" % ('obj', attr), value, type(value) From 18b803d03a31c12d0d73890bed98bc775ff31d33 Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 18:45:28 +0100 Subject: [PATCH 67/89] Typo... --- examples/har_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index 666dc03f0..cc2cf5d7c 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -65,7 +65,7 @@ def response(context, flow): # Get the ssl_time for this server_conn as the difference between the start of the successful # tcp setup and the successful ssl setup. Afterwards add it to seen list, in order to avoid # the ssl_time being present in entries that use an existing connection. If no ssl setup has - # been made initiate it is also left as -1 since it doesn't apply to this connection. + # been made it is also left as -1 since it doesn't apply to this connection. ssl_time = flow.server_conn.timestamp_ssl_setup - flow.server_conn.timestamp_tcp_setup context.seen_server_ssl.add(flow.server_conn) From 57d980712286ce3184a2aad7bef1b63b2b26e95e Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 19:05:36 +0100 Subject: [PATCH 68/89] Added script dependencie to harparser. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 08ccbbfd4..80bb78951 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ script_deps = { "urwid>=1.1", "lxml>=3.3.6", "Pillow>=2.3.0", + "-e git+https://github.com/JustusW/harparser.git#egg=harparser", }, "mitmdump": set() } From 31249b9e2471e05cf3e9eed7fce1ae72cf17451b Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 19:28:10 +0100 Subject: [PATCH 69/89] Hopefully fixed dependency fuckup. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 80bb78951..41598b116 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ script_deps = { "urwid>=1.1", "lxml>=3.3.6", "Pillow>=2.3.0", - "-e git+https://github.com/JustusW/harparser.git#egg=harparser", + "harparser", }, "mitmdump": set() } From 4342d79dc073277b51effda92179ad7050bebf68 Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 20:11:25 +0100 Subject: [PATCH 70/89] Removed the globals and replaced them with internal attributes of _HARLog. Minor bugfixes to make ssl timings work. --- examples/har_extractor.py | 80 ++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index cc2cf5d7c..68fb1d0da 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -18,7 +18,15 @@ class UTC(tzinfo): class _HARLog(HAR.log): - def __init__(self): + __page_list__ = [] + __page_count__ = 0 + __page_ref__ = {} + + def __init__(self, page_list): + self.__page_list__ = page_list + self.__page_count__ = 0 + self.__page_ref__ = {} + HAR.log.__init__(self, {"version": "1.2", "creator": {"name": "MITMPROXY HARExtractor", "version": "0.1", @@ -35,14 +43,27 @@ class _HARLog(HAR.log): if isinstance(obj, HAR.entries): self['entries'].append(obj) + def create_page_id(self): + self.__page_count__ += 1 + return "autopage_%s" % str(self.__page_count__) + + def set_page_ref(self, page, ref): + self.__page_ref__[page] = ref + + def get_page_ref(self, page): + return self.__page_ref__.get(page, None) + + def get_page_list(self): + return self.__page_list__ + def start(context, argv): """ - On start we reset the HAR, it's not really necessary since it will have been - instantiated earlier during initial parsing of this file. You will have to - adapt this to suit your actual needs of HAR generation. + On start we create a HARLog instance. You will have to adapt this to suit your actual needs + of HAR generation. As it will probably be necessary to cluster logs by IPs or reset them + from time to time. """ - HARLog.reset() + context.HARLog = _HARLog(['https://github.com']) context.seen_server_connect = set() context.seen_server_ssl = set() @@ -52,15 +73,15 @@ def response(context, flow): Called when a server response has been received. At the time of this message both a request and a response are present and completely done. """ - connect_time = -1 + connect_time = -.001 if flow.server_conn not in context.seen_server_connect: # Calculate the connect_time for this server_conn. Afterwards add it to seen list, in # order to avoid the connect_time being present in entries that use an existing connection. connect_time = flow.server_conn.timestamp_tcp_setup - flow.server_conn.timestamp_start context.seen_server_connect.add(flow.server_conn) - ssl_time = -1 - if flow.server_conn not in context.seen_server_connect \ + ssl_time = -.001 + if flow.server_conn not in context.seen_server_ssl \ and flow.server_conn.timestamp_ssl_setup is not None: # Get the ssl_time for this server_conn as the difference between the start of the successful # tcp setup and the successful ssl setup. Afterwards add it to seen list, in order to avoid @@ -134,22 +155,23 @@ def response(context, flow): "cache": {}, "timings": timings, }) - # If the current url is in HARPAGE_LIST or does not have a referer we add it as a new pages object. - if flow.request.url in HARPAGE_LIST or flow.request.headers.get('Referer', None) is None: - PAGE_COUNT[1] += 1 - page_id = "_".join([str(v) for v in PAGE_COUNT]) - HARLog.add(HAR.pages({"startedDateTime": entry['startedDateTime'], - "id": page_id, - "title": flow.request.url, })) - PAGE_REF[flow.request.url] = page_id + # If the current url is in the page list of context.HARLog or does not have a referrer we add it as a new + # pages object. + if flow.request.url in context.HARLog.get_page_list() or flow.request.headers.get('Referer', None) is None: + page_id = context.HARLog.create_page_id() + context.HARLog.add(HAR.pages({"startedDateTime": entry['startedDateTime'], + "id": page_id, + "title": flow.request.url, })) + context.HARLog.set_page_ref(flow.request.url, page_id) entry['pageref'] = page_id - # Lookup the referer in our PAGE_REF dict to point this entries pageref attribute to the right pages object. - elif flow.request.headers.get('Referer', (None, ))[0] in PAGE_REF.keys(): - entry['pageref'] = PAGE_REF[flow.request.headers['Referer'][0]] - PAGE_REF[flow.request.url] = entry['pageref'] + # Lookup the referer in the page_ref of context.HARLog to point this entries pageref attribute to the right + # pages object, then set it as a new reference to build a reference tree. + elif context.HARLog.get_page_ref(flow.request.headers.get('Referer', (None, ))[0]) is not None: + entry['pageref'] = context.HARLog.get_page_ref(flow.request.headers['Referer'][0]) + context.HARLog.set_page_ref(flow.request.headers['Referer'][0], entry['pageref']) - HARLog.add(entry) + context.HARLog.add(entry) def done(context): @@ -159,8 +181,8 @@ def done(context): from pprint import pprint import json - json_dump = HARLog.json() - compressed_json_dump = HARLog.compress() + json_dump = context.HARLog.json() + compressed_json_dump = context.HARLog.compress() print "=" * 100 pprint(json.loads(json_dump)) @@ -180,14 +202,4 @@ def print_attributes(obj, filter_string=None, hide_privates=False): if filter_string is not None and filter_string not in attr: continue value = getattr(obj, attr) - print "%s.%s" % ('obj', attr), value, type(value) - - -# Some initializations. Add any page you want to have its own pages object to HARPAGE_LIST -HARPAGE_LIST = ['https://github.com/'] -HARLog = _HARLog() - -CONNECT_TIMES = {} -SSL_TIMES = {} -PAGE_REF = {} -PAGE_COUNT = ['autopage', 0] + print "%s.%s" % ('obj', attr), value, type(value) \ No newline at end of file From 4227feef37b9a9c0e835ebf179b5fb7a4509569e Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 21:14:50 +0100 Subject: [PATCH 71/89] It seems get_decoded_content can actually be shorter than content due to encoding issues. Since I'm not crazy after all it seems safe to push. --- examples/har_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index 68fb1d0da..4de8ce6a3 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -126,7 +126,7 @@ def response(context, flow): response_headers = [{"name": k, "value": v} for k, v in flow.response.headers] response_headers_size = len(str(flow.response.headers)) response_body_size = len(flow.response.content) - response_body_decoded_size = len(flow.response.content) + response_body_decoded_size = len(flow.response.get_decoded_content()) response_body_compression = response_body_decoded_size - response_body_size response_mime_type = flow.response.headers.get('Content-Type', [''])[0] response_redirect_url = flow.response.headers.get('Location', [''])[0] From a7ab06d80eccbe3e58753da0917fca8d55a21c8e Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 22:04:52 +0100 Subject: [PATCH 72/89] Switched to pytz. Added comment for clarification on behaviour of HAREncodable. Added missing parameter in reset(). Fixed accessing headers. --- examples/har_extractor.py | 23 ++++++++--------------- setup.py | 1 + 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index 4de8ce6a3..8e97ee2d3 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -2,22 +2,15 @@ This inline script utilizes harparser.HAR from https://github.com/JustusW/harparser to generate a HAR log object. """ +from pytz import utc from harparser import HAR from datetime import datetime, timedelta, tzinfo -class UTC(tzinfo): - def utcoffset(self, dt): - return timedelta(0) - - def dst(self, dt): - return timedelta(0) - - def tzname(self, dt): - return "Z" - - class _HARLog(HAR.log): + # The attributes need to be registered here for them to actually be available later via self. This is + # due to HAREncodable linking __getattr__ to __getitem__. Anything that is set only in __init__ will + # just be added as key/value pair to self.__classes__. __page_list__ = [] __page_count__ = 0 __page_ref__ = {} @@ -35,7 +28,7 @@ class _HARLog(HAR.log): "entries": []}) def reset(self): - self.__init__() + self.__init__(self.__page_list__) def add(self, obj): if isinstance(obj, HAR.pages): @@ -110,7 +103,7 @@ def response(context, flow): if item > -1: full_time += item - started_date_time = datetime.fromtimestamp(flow.request.timestamp_start, tz=UTC()).isoformat() + started_date_time = datetime.fromtimestamp(flow.request.timestamp_start, tz=utc).isoformat() request_query_string = [{"name": k, "value": v} for k, v in flow.request.get_query()] request_http_version = ".".join([str(v) for v in flow.request.httpversion]) @@ -128,8 +121,8 @@ def response(context, flow): response_body_size = len(flow.response.content) response_body_decoded_size = len(flow.response.get_decoded_content()) response_body_compression = response_body_decoded_size - response_body_size - response_mime_type = flow.response.headers.get('Content-Type', [''])[0] - response_redirect_url = flow.response.headers.get('Location', [''])[0] + response_mime_type = flow.response.headers.get_first('Content-Type', '') + response_redirect_url = flow.response.headers.get_first('Location', '') entry = HAR.entries({"startedDateTime": started_date_time, "time": full_time, diff --git a/setup.py b/setup.py index 41598b116..7ea6d0cb5 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ script_deps = { "urwid>=1.1", "lxml>=3.3.6", "Pillow>=2.3.0", + "pytz", "harparser", }, "mitmdump": set() From c84ad384f660ba2c04aad3dfd3e7d3b961b013aa Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 22:37:32 +0100 Subject: [PATCH 73/89] Updated setup.py and moved requirements to examples section. Included examples section in requirements.txt. Updated har_extractor to use command line arguments. --- examples/har_extractor.py | 36 ++++++++++++++++++++++-------------- requirements.txt | 2 +- setup.py | 6 ++++-- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index 8e97ee2d3..c994f3718 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -56,9 +56,14 @@ def start(context, argv): of HAR generation. As it will probably be necessary to cluster logs by IPs or reset them from time to time. """ + context.dump_file = None + if len(argv) > 1: + context.dump_file = argv[1] + else: + raise ValueError('Usage: -s "har_extractor.py filename" ' + '(- will output to stdout, filenames ending with .zhar will result in compressed har)') context.HARLog = _HARLog(['https://github.com']) - context.seen_server_connect = set() - context.seen_server_ssl = set() + context.seen_server = set() def response(context, flow): @@ -66,22 +71,20 @@ def response(context, flow): Called when a server response has been received. At the time of this message both a request and a response are present and completely done. """ + # Values are converted from float seconds to int milliseconds later. + ssl_time = -.001 connect_time = -.001 - if flow.server_conn not in context.seen_server_connect: + if flow.server_conn not in context.seen_server: # Calculate the connect_time for this server_conn. Afterwards add it to seen list, in # order to avoid the connect_time being present in entries that use an existing connection. connect_time = flow.server_conn.timestamp_tcp_setup - flow.server_conn.timestamp_start - context.seen_server_connect.add(flow.server_conn) + context.seen_server.add(flow.server_conn) - ssl_time = -.001 - if flow.server_conn not in context.seen_server_ssl \ - and flow.server_conn.timestamp_ssl_setup is not None: - # Get the ssl_time for this server_conn as the difference between the start of the successful - # tcp setup and the successful ssl setup. Afterwards add it to seen list, in order to avoid - # the ssl_time being present in entries that use an existing connection. If no ssl setup has - # been made it is also left as -1 since it doesn't apply to this connection. - ssl_time = flow.server_conn.timestamp_ssl_setup - flow.server_conn.timestamp_tcp_setup - context.seen_server_ssl.add(flow.server_conn) + if flow.server_conn.timestamp_ssl_setup is not None: + # Get the ssl_time for this server_conn as the difference between the start of the successful + # tcp setup and the successful ssl setup. If no ssl setup has been made it is left as -1 since + # it doesn't apply to this connection. + ssl_time = flow.server_conn.timestamp_ssl_setup - flow.server_conn.timestamp_tcp_setup # Calculate the raw timings from the different timestamps present in the request and response object. # For lack of a way to measure it dns timings can not be calculated. The same goes for HAR blocked: @@ -178,7 +181,12 @@ def done(context): compressed_json_dump = context.HARLog.compress() print "=" * 100 - pprint(json.loads(json_dump)) + if context.dump_file == '-': + pprint(json.loads(json_dump)) + elif context.dump_file.endswith('.zhar'): + file(context.dump_file, "w").write(compressed_json_dump) + else: + file(context.dump_file, "w").write(json_dump) print "=" * 100 print "HAR log finished with %s bytes (%s bytes compressed)" % (len(json_dump), len(compressed_json_dump)) print "Compression rate is %s%%" % str(100. * len(compressed_json_dump) / len(json_dump)) diff --git a/requirements.txt b/requirements.txt index d84347b73..946e5ffe8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -e git+https://github.com/mitmproxy/netlib.git#egg=netlib -e git+https://github.com/mitmproxy/pathod.git#egg=pathod --e .[dev] \ No newline at end of file +-e .[dev,examples] \ No newline at end of file diff --git a/setup.py b/setup.py index 7ea6d0cb5..ace5e8174 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,6 @@ script_deps = { "urwid>=1.1", "lxml>=3.3.6", "Pillow>=2.3.0", - "pytz", - "harparser", }, "mitmdump": set() } @@ -80,6 +78,10 @@ setup( "pyamf>=0.6.1", "protobuf>=2.5.0", "cssutils>=1.0" + ], + 'examples': [ + "pytz", + "harparser", ] } ) From ddce662fe64a693f64f9fda4b5e406be8f1278d1 Mon Sep 17 00:00:00 2001 From: Justus Wingert Date: Sat, 15 Nov 2014 22:39:15 +0100 Subject: [PATCH 74/89] Added try/except block for import errors with harparser and pytz. --- examples/har_extractor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/har_extractor.py b/examples/har_extractor.py index c994f3718..531f32aaa 100644 --- a/examples/har_extractor.py +++ b/examples/har_extractor.py @@ -2,8 +2,14 @@ This inline script utilizes harparser.HAR from https://github.com/JustusW/harparser to generate a HAR log object. """ -from pytz import utc -from harparser import HAR +try: + from harparser import HAR + from pytz import UTC +except ImportError as e: + import sys + print >> sys.stderr, "\r\nMissing dependencies: please run `pip install mitmproxy[examples]`.\r\n" + raise + from datetime import datetime, timedelta, tzinfo From 667fe0c20b43e2c5af5380591015b122da79b013 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 15 Nov 2014 23:10:25 +0100 Subject: [PATCH 75/89] fix tests --- setup.py | 1 + test/test_examples.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1d3c62051..79398a186 100644 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ setup( 'examples': [ "pytz", "harparser", + "beautifulsoup4" ] } ) diff --git a/test/test_examples.py b/test/test_examples.py index fd42e6f08..a5a212cd1 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -1,10 +1,8 @@ import glob -import mock from libmproxy import utils, script from libmproxy.proxy import config import tservers -@mock.patch.dict("sys.modules", {"bs4": mock.Mock()}) def test_load_scripts(): example_dir = utils.Data("libmproxy").path("../examples") scripts = glob.glob("%s/*.py" % example_dir) @@ -12,6 +10,8 @@ def test_load_scripts(): tmaster = tservers.TestMaster(config.ProxyConfig()) for f in scripts: + if "har_extractor" in f: + f += " foo" if "iframe_injector" in f: f += " foo" # one argument required if "modify_response_body" in f: From ec235941919c184641f9ab30f2df13ab7fea0414 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 19 Nov 2014 01:27:20 +0100 Subject: [PATCH 76/89] add sni support to LiveConnection.change_server --- libmproxy/protocol/primitives.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py index 1bf7f832d..3be1cc45c 100644 --- a/libmproxy/protocol/primitives.py +++ b/libmproxy/protocol/primitives.py @@ -174,7 +174,7 @@ class LiveConnection(object): self._backup_server_conn = None """@type: libmproxy.proxy.connection.ServerConnection""" - def change_server(self, address, ssl=None, force=False, persistent_change=False): + def change_server(self, address, ssl=None, sni=None, force=False, persistent_change=False): """ Change the server connection to the specified address. @returns: @@ -183,7 +183,14 @@ class LiveConnection(object): """ address = netlib.tcp.Address.wrap(address) - ssl_mismatch = (ssl is not None and ssl != self.c.server_conn.ssl_established) + ssl_mismatch = ( + ssl is not None and + ( + ssl != self.c.server_conn.ssl_established + or + (sni is not None and sni != self.c.sni) + ) + ) address_mismatch = (address != self.c.server_conn.address) if persistent_change: @@ -212,6 +219,8 @@ class LiveConnection(object): self.c.set_server_address(address) self.c.establish_server_connection(ask=False) + if sni: + self.c.sni = sni if ssl: self.c.establish_ssl(server=True) return True From f7c5385679a6e10f707b744242dc8edcf1028bf7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 22 Nov 2014 15:27:43 +0100 Subject: [PATCH 77/89] retain raw filter str on filt objects --- libmproxy/filt.py | 4 +++- libmproxy/flow.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/libmproxy/filt.py b/libmproxy/filt.py index 7d2bd737f..5d2590965 100644 --- a/libmproxy/filt.py +++ b/libmproxy/filt.py @@ -343,7 +343,9 @@ bnf = _make() def parse(s): try: - return bnf.parseString(s, parseAll=True)[0] + filt = bnf.parseString(s, parseAll=True)[0] + filt.pattern = s + return filt except pp.ParseException: return None except ValueError: diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 007136985..5abcb1ab1 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -346,11 +346,13 @@ class State(object): # These are compiled filt expressions: self._limit = None self.intercept = None - self._limit_txt = None @property def limit_txt(self): - return self._limit_txt + if self.filt: + return self.filt.pattern + else: + return None def flow_count(self): return len(self._flow_list) @@ -407,10 +409,8 @@ class State(object): if not f: return "Invalid filter expression." self._limit = f - self._limit_txt = txt else: self._limit = None - self._limit_txt = None self.recalculate_view() def set_intercept(self, txt): From 47a78e3c729f4ddb7971b72bfae30140562f4dd6 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 23 Nov 2014 15:46:17 +0100 Subject: [PATCH 78/89] fix limit_txt, fix #412 --- libmproxy/flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 5abcb1ab1..a6bf17d8e 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -349,8 +349,8 @@ class State(object): @property def limit_txt(self): - if self.filt: - return self.filt.pattern + if self._limit: + return self._limit.pattern else: return None From 3887e7ed29db88fd9f18d42013346c5dce5aa083 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 26 Nov 2014 04:56:17 +0100 Subject: [PATCH 79/89] fix error html --- libmproxy/protocol/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 26a94040c..386a36662 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1198,7 +1198,7 @@ class HTTPHandler(ProtocolHandler): %d %s - + %s """ % (code, response, message) self.c.client_conn.wfile.write("HTTP/1.1 %s %s\r\n" % (code, response)) From 56f1278d1a0233620289d2fa4173449ed108a73e Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 17:52:54 +0100 Subject: [PATCH 80/89] fix #413 --- libmproxy/console/grideditor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index 72c1e4a05..438d0ad7a 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -123,7 +123,6 @@ class GridWalker(urwid.ListWalker): except ValueError: self.editor.master.statusbar.message("Invalid Python-style string encoding.", 1000) return - errors = self.lst[self.focus][1] emsg = self.editor.is_error(self.focus_col, val) if emsg: @@ -322,9 +321,11 @@ class GridEditor(common.WWrap): elif key == "d": self.walker.delete_focus() elif key == "r": - self.master.path_prompt("Read file: ", "", self.read_file) + if self.walker.get_current_value() is not None: + self.master.path_prompt("Read file: ", "", self.read_file) elif key == "R": - self.master.path_prompt("Read unescaped file: ", "", self.read_file, True) + if self.walker.get_current_value() is not None: + self.master.path_prompt("Read unescaped file: ", "", self.read_file, True) elif key == "e": o = self.walker.get_current_value() if o is not None: From cf7404cfe0579483e2478c816ca883048ad1ece1 Mon Sep 17 00:00:00 2001 From: Gabriel Kirkpatrick Date: Fri, 28 Nov 2014 12:36:31 -0500 Subject: [PATCH 81/89] Changed argument name from confdir to cadir in flowbasic example --- examples/flowbasic | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flowbasic b/examples/flowbasic index 41402b0cc..c71debc93 100755 --- a/examples/flowbasic +++ b/examples/flowbasic @@ -36,7 +36,7 @@ class MyMaster(flow.FlowMaster): config = proxy.ProxyConfig( port=8080, - confdir="~/.mitmproxy/" # use ~/.mitmproxy/mitmproxy-ca.pem as default CA file. + cadir="~/.mitmproxy/" # use ~/.mitmproxy/mitmproxy-ca.pem as default CA file. ) state = flow.State() server = ProxyServer(config) From 3b03758ef8b8650ccc9449a7507f184059859afa Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Sat, 29 Nov 2014 04:13:29 -0800 Subject: [PATCH 82/89] README: Fixed double-quote and whitespace issues --- README.mkd | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.mkd b/README.mkd index ccc09138b..bc18cb488 100644 --- a/README.mkd +++ b/README.mkd @@ -72,8 +72,7 @@ This installs the latest GitHub versions of mitmproxy, netlib and pathod into `m The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py) and [pathod](http://pathod.net), version matching mitmproxy. Install these with: -` -pip install "mitmproxy[dev]""` +`pip install "mitmproxy[dev]"` Please ensure that all patches are accompanied by matching changes in the test @@ -85,8 +84,3 @@ suite. The project maintains 100% test coverage. Rendering the documentation requires [countershape](http://github.com/cortesi/countershape). After installation, you can render the documentation to the doc like this: `cshape doc-src doc` - - - - - From 992536c2bc0afa5da81e82cfcd8953663559ff59 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 1 Dec 2014 02:28:03 +0100 Subject: [PATCH 83/89] make header processing configurable by inline scripts, refs #340 --- libmproxy/protocol/http.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 386a36662..89af85b06 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -419,17 +419,19 @@ class HTTPRequest(HTTPMessage): raise http.HttpError(400, "Invalid request form") return request_line + # This list is adopted legacy code. + # We probably don't need to strip off keep-alive. + _headers_to_strip_off = ['Proxy-Connection', + 'Keep-Alive', + 'Connection', + 'Transfer-Encoding', + 'Upgrade'] + def _assemble_headers(self): headers = self.headers.copy() - for k in ['Proxy-Connection', - 'Keep-Alive', - 'Connection', - 'Transfer-Encoding']: + for k in self._headers_to_strip_off: del headers[k] - if headers["Upgrade"] == ["h2c"]: - # Suppress HTTP2 https://http2.github.io/http2-spec/index.html#discover-http - del headers["Upgrade"] - if not 'host' in headers and self.scheme and self.host and self.port: + if 'host' not in headers and self.scheme and self.host and self.port: headers["Host"] = [utils.hostport(self.scheme, self.host, self.port)] @@ -750,11 +752,13 @@ class HTTPResponse(HTTPMessage): return 'HTTP/%s.%s %s %s' % \ (self.httpversion[0], self.httpversion[1], self.code, self.msg) + _headers_to_strip_off = ['Proxy-Connection', + 'Alternate-Protocol', + 'Alt-Svc'] + def _assemble_headers(self, preserve_transfer_encoding=False): headers = self.headers.copy() - for k in ['Proxy-Connection', - 'Alternate-Protocol', - 'Alt-Svc']: + for k in self._headers_to_strip_off: del headers[k] if not preserve_transfer_encoding: del headers['Transfer-Encoding'] From 5b1fefee9bf8564b32a1137975cb181d54ef6dff Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 1 Dec 2014 03:04:48 +0100 Subject: [PATCH 84/89] add inline script example for websocket passthrough, fix #340 --- examples/ignore_websocket.py | 27 +++++++++++++++++++++++++++ libmproxy/protocol/http.py | 6 ++++++ 2 files changed, 33 insertions(+) create mode 100644 examples/ignore_websocket.py diff --git a/examples/ignore_websocket.py b/examples/ignore_websocket.py new file mode 100644 index 000000000..1ee81d38e --- /dev/null +++ b/examples/ignore_websocket.py @@ -0,0 +1,27 @@ +# This script makes mitmproxy switch to passthrough mode for all HTTP +# responses with "Connection: Upgrade" header. This is useful to make +# WebSockets work in untrusted environments. +# +# Note: Chrome (and possibly other browsers), when explicitly configured +# to use a proxy (i.e. mitmproxy's regular mode), send a CONNECT request +# to the proxy before they initiate the websocket connection. +# To make WebSockets work in these cases, supply +# `--ignore :80$` as an additional parameter. +# (see http://mitmproxy.org/doc/features/passthrough.html) + +from libmproxy.protocol.http import HTTPRequest +from libmproxy.protocol.tcp import TCPHandler +from libmproxy.protocol import KILL +from libmproxy.script import concurrent + +HTTPRequest._headers_to_strip_off.remove("Connection") +HTTPRequest._headers_to_strip_off.remove("Upgrade") + +@concurrent +def response(context, flow): + if flow.response.headers.get_first("Connection", None) == "Upgrade": + # We need to send the response manually now... + flow.client_conn.send(flow.response.assemble()) + # ...and then delegate to tcp passthrough. + TCPHandler(flow.live.c, log=False).handle_messages() + flow.reply(KILL) \ No newline at end of file diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 89af85b06..87af8e6d1 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1401,6 +1401,12 @@ class HTTPHandler(ProtocolHandler): # In practice, nobody issues a CONNECT request to send unencrypted HTTP requests afterwards. # If we don't delegate to TCP mode, we should always negotiate a SSL connection. + # + # FIXME: + # Turns out the previous statement isn't entirely true. Chrome on Windows CONNECTs to :80 + # if an explicit proxy is configured and a websocket connection should be established. + # We don't support websocket at the moment, so it fails anyway, but we should come up with + # a better solution to this if we start to support WebSockets. should_establish_ssl = ( address.port in self.c.config.ssl_ports or From a7a9ef826c206ca93c20ae26c20f5e5a2d5de8e6 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 1 Dec 2014 03:36:04 +0100 Subject: [PATCH 85/89] fix tests --- examples/ignore_websocket.py | 23 +++++++++++++++-------- test/test_examples.py | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/examples/ignore_websocket.py b/examples/ignore_websocket.py index 1ee81d38e..48093951e 100644 --- a/examples/ignore_websocket.py +++ b/examples/ignore_websocket.py @@ -14,14 +14,21 @@ from libmproxy.protocol.tcp import TCPHandler from libmproxy.protocol import KILL from libmproxy.script import concurrent -HTTPRequest._headers_to_strip_off.remove("Connection") -HTTPRequest._headers_to_strip_off.remove("Upgrade") + +def start(context, argv): + HTTPRequest._headers_to_strip_off.remove("Connection") + HTTPRequest._headers_to_strip_off.remove("Upgrade") + + +def done(context): + HTTPRequest._headers_to_strip_off.append("Connection") + HTTPRequest._headers_to_strip_off.append("Upgrade") @concurrent def response(context, flow): - if flow.response.headers.get_first("Connection", None) == "Upgrade": - # We need to send the response manually now... - flow.client_conn.send(flow.response.assemble()) - # ...and then delegate to tcp passthrough. - TCPHandler(flow.live.c, log=False).handle_messages() - flow.reply(KILL) \ No newline at end of file + if flow.response.headers.get_first("Connection", None) == "Upgrade": + # We need to send the response manually now... + flow.client_conn.send(flow.response.assemble()) + # ...and then delegate to tcp passthrough. + TCPHandler(flow.live.c, log=False).handle_messages() + flow.reply(KILL) \ No newline at end of file diff --git a/test/test_examples.py b/test/test_examples.py index a5a212cd1..deb97b493 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -16,4 +16,5 @@ def test_load_scripts(): f += " foo" # one argument required if "modify_response_body" in f: f += " foo bar" # two arguments required - script.Script(f, tmaster) # Loads the script file. \ No newline at end of file + s = script.Script(f, tmaster) # Loads the script file. + s.unload() \ No newline at end of file From 591ed0b41f91e2c688046de68d42115004328a96 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 4 Dec 2014 00:29:15 +0100 Subject: [PATCH 86/89] fix HTTPResponse creation --- libmproxy/console/flowview.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 3dceff704..1ec57a4e5 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -574,9 +574,8 @@ class FlowView(common.WWrap): else: if not self.flow.response: self.flow.response = HTTPResponse( - self.flow.request, self.flow.request.httpversion, - 200, "OK", flow.ODictCaseless(), "", None + 200, "OK", flow.ODictCaseless(), "" ) self.flow.response.reply = controller.DummyReply() conn = self.flow.response From 31925dc9bedde9fcb388d5254b43ac90a12d4eaf Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 8 Dec 2014 17:01:47 +0100 Subject: [PATCH 87/89] fix #419 --- libmproxy/onboarding/templates/index.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libmproxy/onboarding/templates/index.html b/libmproxy/onboarding/templates/index.html index 50cfd5dbd..65fda5d2b 100644 --- a/libmproxy/onboarding/templates/index.html +++ b/libmproxy/onboarding/templates/index.html @@ -1,5 +1,5 @@ {% extends "frame.html" %} -{% block body %} +{% block body %}

Click to install the mitmproxy certificate:

@@ -23,4 +23,13 @@ +
+
+ Other mitmproxy users cannot intercept your connection. +
+
+ This page is served by your local mitmproxy instance. The certificate you are about to install has been uniquely generated on mitmproxy's first run and is not shared + between mitmproxy installations. +
+ {% endblock %} From b95f0c997127bb0516cde0609c83c7d2af5ccfbc Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 8 Dec 2014 17:17:37 +0100 Subject: [PATCH 88/89] fix #411 --- libmproxy/protocol/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 87af8e6d1..49f5e8c03 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1043,7 +1043,7 @@ class HTTPHandler(ProtocolHandler): # call the appropriate script hook - this is an opportunity for an # inline script to set flow.stream = True flow = self.c.channel.ask("responseheaders", flow) - if flow == KILL: + if flow is None or flow == KILL: raise KillSignal() else: # now get the rest of the request body, if body still needs to be @@ -1086,11 +1086,11 @@ class HTTPHandler(ProtocolHandler): # sent through to the Master. flow.request = req request_reply = self.c.channel.ask("request", flow) - self.process_server_address(flow) # The inline script may have changed request.host - if request_reply is None or request_reply == KILL: raise KillSignal() + self.process_server_address(flow) # The inline script may have changed request.host + if isinstance(request_reply, HTTPResponse): flow.response = request_reply else: From ffb95a1db742d71d7671f9e9c6db552774bb0ead Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 9 Dec 2014 00:01:01 +0100 Subject: [PATCH 89/89] fix #304 --- doc-src/features/reverseproxy.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc-src/features/reverseproxy.html b/doc-src/features/reverseproxy.html index 1c57f0b23..5ef4efc52 100644 --- a/doc-src/features/reverseproxy.html +++ b/doc-src/features/reverseproxy.html @@ -25,4 +25,28 @@ service uses HTTP like this: https2http://hostname:port +### Host Header +In reverse proxy mode, mitmproxy does not rewrite the host header. While often useful, this +may lead to issues with public web servers. For example, consider the following scenario: + + $ python mitmdump -d -R http://example.com/ & + $ curl http://localhost:8080/ + + >> GET https://example.com/ + Host: localhost:8080 + User-Agent: curl/7.35.0 + [...] + + << 404 Not Found 345B + +Since the Host header doesn't match example.com, an error is returned.
+There are two ways to solve this: +
    +
  1. Modify the hosts file of your OS so that example.com resolves to 127.0.0.1.
  2. +
  3. + Instruct mitmproxy to rewrite the host header by passing ‑‑setheader :~q:Host:example.com. + However, keep in mind that absolute URLs within the returned document or HTTP redirects will cause the client application + to bypass the proxy. +
  4. +
\ No newline at end of file