diff options
-rw-r--r-- | docs/scripting/events.rst | 8 | ||||
-rw-r--r-- | docs/transparent/osx.rst | 3 | ||||
-rw-r--r-- | mitmproxy/addons/core.py | 3 | ||||
-rw-r--r-- | mitmproxy/connections.py | 18 | ||||
-rw-r--r-- | mitmproxy/net/http/cookies.py | 51 | ||||
-rw-r--r-- | mitmproxy/net/tls.py | 21 | ||||
-rw-r--r-- | mitmproxy/proxy/protocol/http_replay.py | 10 | ||||
-rw-r--r-- | mitmproxy/proxy/protocol/tls.py | 15 | ||||
-rw-r--r-- | mitmproxy/tools/console/commander/commander.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/console/consoleaddons.py | 13 | ||||
-rw-r--r-- | mitmproxy/tools/console/defaultkeys.py | 6 | ||||
-rw-r--r-- | setup.py | 4 | ||||
-rw-r--r-- | test/mitmproxy/net/http/test_cookies.py | 30 | ||||
-rw-r--r-- | test/mitmproxy/net/http/test_response.py | 2 | ||||
-rw-r--r-- | test/mitmproxy/test_connections.py | 9 | ||||
-rw-r--r-- | test/mitmproxy/tools/console/test_keymap.py | 2 | ||||
-rw-r--r-- | web/package.json | 3 | ||||
-rw-r--r-- | web/src/js/components/FlowTable/FlowColumns.jsx | 2 | ||||
-rw-r--r-- | web/src/js/ducks/utils/store.js | 6 | ||||
-rw-r--r-- | web/yarn.lock | 4 |
20 files changed, 134 insertions, 78 deletions
diff --git a/docs/scripting/events.rst b/docs/scripting/events.rst index 4d74b220..d8b1fbb8 100644 --- a/docs/scripting/events.rst +++ b/docs/scripting/events.rst @@ -100,10 +100,10 @@ HTTP Events * - .. py:function:: http_connect(flow) - Called when we receive an HTTP CONNECT request. Setting a non 2xx - response on the flow will return the response to the client abort the - connection. CONNECT requests and responses do not generate the usual - HTTP handler events. CONNECT requests are only valid in regular and - upstream proxy modes. + response on the flow will return the response to the client and abort + the connection. CONNECT requests and responses do not generate the + usual HTTP handler events. CONNECT requests are only valid in regular + and upstream proxy modes. *flow* A ``models.HTTPFlow`` object. The flow is guaranteed to have diff --git a/docs/transparent/osx.rst b/docs/transparent/osx.rst index 40e91fac..5d4ec612 100644 --- a/docs/transparent/osx.rst +++ b/docs/transparent/osx.rst @@ -17,8 +17,7 @@ Note that this means we don't support transparent mode for earlier versions of O .. code-block:: none - rdr on en2 inet proto tcp to any port 80 -> 127.0.0.1 port 8080 - rdr on en2 inet proto tcp to any port 443 -> 127.0.0.1 port 8080 + rdr on en0 inet proto tcp to any port {80, 443} -> 127.0.0.1 port 8080 These rules tell pf to redirect all traffic destined for port 80 or 443 to the local mitmproxy instance running on port 8080. You should diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index 2b0b2f14..ca21e1dc 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -164,6 +164,7 @@ class Core: for f in flows: p = getattr(f, part, None) if p: + f.backup() p.decode() updated.append(f) ctx.master.addons.trigger("update", updated) @@ -178,6 +179,7 @@ class Core: for f in flows: p = getattr(f, part, None) if p: + f.backup() current_enc = p.headers.get("content-encoding", "identity") if current_enc == "identity": p.encode("deflate") @@ -204,6 +206,7 @@ class Core: if p: current_enc = p.headers.get("content-encoding", "identity") if current_enc == "identity": + f.backup() p.encode(enc) updated.append(f) ctx.master.addons.trigger("update", updated) diff --git a/mitmproxy/connections.py b/mitmproxy/connections.py index 86565b7b..9c47985c 100644 --- a/mitmproxy/connections.py +++ b/mitmproxy/connections.py @@ -253,7 +253,7 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): address=address, ip_address=address, cert=None, - sni=None, + sni=address[0], alpn_proto_negotiated=None, tls_version=None, source_address=('', 0), @@ -276,21 +276,21 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): self.wfile.write(message) self.wfile.flush() - def establish_tls(self, clientcerts, sni, **kwargs): + def establish_tls(self, *, sni=None, client_certs=None, **kwargs): if sni and not isinstance(sni, str): raise ValueError("sni must be str, not " + type(sni).__name__) - clientcert = None - if clientcerts: - if os.path.isfile(clientcerts): - clientcert = clientcerts + client_cert = None + if client_certs: + if os.path.isfile(client_certs): + client_cert = client_certs else: path = os.path.join( - clientcerts, + client_certs, self.address[0].encode("idna").decode()) + ".pem" if os.path.exists(path): - clientcert = path + client_cert = path - self.convert_to_tls(cert=clientcert, sni=sni, **kwargs) + self.convert_to_tls(cert=client_cert, sni=sni, **kwargs) self.sni = sni self.alpn_proto_negotiated = self.get_alpn_proto_negotiated() self.tls_version = self.connection.get_protocol_version_name() diff --git a/mitmproxy/net/http/cookies.py b/mitmproxy/net/http/cookies.py index 4824bf56..7bef8757 100644 --- a/mitmproxy/net/http/cookies.py +++ b/mitmproxy/net/http/cookies.py @@ -114,11 +114,10 @@ def _read_cookie_pairs(s, off=0): lhs, off = _read_key(s, off) lhs = lhs.lstrip() - if lhs: - rhs = None - if off < len(s) and s[off] == "=": - rhs, off = _read_value(s, off + 1, ";") - + rhs = "" + if off < len(s) and s[off] == "=": + rhs, off = _read_value(s, off + 1, ";") + if rhs or lhs: pairs.append([lhs, rhs]) off += 1 @@ -143,25 +142,24 @@ def _read_set_cookie_pairs(s: str, off=0) -> Tuple[List[TPairs], int]: lhs, off = _read_key(s, off, ";=,") lhs = lhs.lstrip() - if lhs: - rhs = None - if off < len(s) and s[off] == "=": - rhs, off = _read_value(s, off + 1, ";,") - - # Special handliing of attributes - if lhs.lower() == "expires": - # 'expires' values can contain commas in them so they need to - # be handled separately. + rhs = "" + if off < len(s) and s[off] == "=": + rhs, off = _read_value(s, off + 1, ";,") - # We actually bank on the fact that the expires value WILL - # contain a comma. Things will fail, if they don't. + # Special handling of attributes + if lhs.lower() == "expires": + # 'expires' values can contain commas in them so they need to + # be handled separately. - # '3' is just a heuristic we use to determine whether we've - # only read a part of the expires value and we should read more. - if len(rhs) <= 3: - trail, off = _read_value(s, off + 1, ";,") - rhs = rhs + "," + trail + # We actually bank on the fact that the expires value WILL + # contain a comma. Things will fail, if they don't. + # '3' is just a heuristic we use to determine whether we've + # only read a part of the expires value and we should read more. + if len(rhs) <= 3: + trail, off = _read_value(s, off + 1, ";,") + rhs = rhs + "," + trail + if rhs or lhs: pairs.append([lhs, rhs]) # comma marks the beginning of a new cookie @@ -196,13 +194,10 @@ def _format_pairs(pairs, specials=(), sep="; "): """ vals = [] for k, v in pairs: - if v is None: - vals.append(k) - else: - if k.lower() not in specials and _has_special(v): - v = ESCAPE.sub(r"\\\1", v) - v = '"%s"' % v - vals.append("%s=%s" % (k, v)) + if k.lower() not in specials and _has_special(v): + v = ESCAPE.sub(r"\\\1", v) + v = '"%s"' % v + vals.append("%s=%s" % (k, v)) return sep.join(vals) diff --git a/mitmproxy/net/tls.py b/mitmproxy/net/tls.py index 0e43a2ac..f8eeb44b 100644 --- a/mitmproxy/net/tls.py +++ b/mitmproxy/net/tls.py @@ -13,6 +13,7 @@ import certifi from OpenSSL import SSL from kaitaistruct import KaitaiStream +import mitmproxy.options # noqa from mitmproxy import exceptions, certs from mitmproxy.contrib.kaitaistruct import tls_client_hello from mitmproxy.net import check @@ -57,6 +58,26 @@ METHOD_NAMES = { } +def client_arguments_from_options(options: "mitmproxy.options.Options") -> dict: + + if options.ssl_insecure: + verify = SSL.VERIFY_NONE + else: + verify = SSL.VERIFY_PEER + + method, tls_options = VERSION_CHOICES[options.ssl_version_server] + + return { + "verify": verify, + "method": method, + "options": tls_options, + "ca_path": options.ssl_verify_upstream_trusted_cadir, + "ca_pemfile": options.ssl_verify_upstream_trusted_ca, + "client_certs": options.client_certs, + "cipher_list": options.ciphers_server, + } + + class MasterSecretLogger: def __init__(self, filename): self.filename = filename diff --git a/mitmproxy/proxy/protocol/http_replay.py b/mitmproxy/proxy/protocol/http_replay.py index 022e8133..0f3be1ea 100644 --- a/mitmproxy/proxy/protocol/http_replay.py +++ b/mitmproxy/proxy/protocol/http_replay.py @@ -9,7 +9,7 @@ from mitmproxy import http from mitmproxy import flow from mitmproxy import options from mitmproxy import connections -from mitmproxy.net import server_spec +from mitmproxy.net import server_spec, tls from mitmproxy.net.http import http1 from mitmproxy.coretypes import basethread from mitmproxy.utils import human @@ -76,8 +76,8 @@ class RequestReplayThread(basethread.BaseThread): if resp.status_code != 200: raise exceptions.ReplayException("Upstream server refuses CONNECT request") server.establish_tls( - self.options.client_certs, - sni=self.f.server_conn.sni + sni=self.f.server_conn.sni, + **tls.client_arguments_from_options(self.options) ) r.first_line_format = "relative" else: @@ -91,8 +91,8 @@ class RequestReplayThread(basethread.BaseThread): server.connect() if r.scheme == "https": server.establish_tls( - self.options.client_certs, - sni=self.f.server_conn.sni + sni=self.f.server_conn.sni, + **tls.client_arguments_from_options(self.options) ) r.first_line_format = "relative" diff --git a/mitmproxy/proxy/protocol/tls.py b/mitmproxy/proxy/protocol/tls.py index d04c9801..876c1162 100644 --- a/mitmproxy/proxy/protocol/tls.py +++ b/mitmproxy/proxy/protocol/tls.py @@ -424,6 +424,9 @@ class TlsLayer(base.Layer): # * which results in garbage because the layers don' match. alpn = [self.client_conn.get_alpn_proto_negotiated()] + # We pass through the list of ciphers send by the client, because some HTTP/2 servers + # will select a non-HTTP/2 compatible cipher from our default list and then hang up + # because it's incompatible with h2. :-) ciphers_server = self.config.options.ciphers_server if not ciphers_server and self._client_tls: ciphers_server = [] @@ -432,16 +435,12 @@ class TlsLayer(base.Layer): ciphers_server.append(CIPHER_ID_NAME_MAP[id]) ciphers_server = ':'.join(ciphers_server) + args = net_tls.client_arguments_from_options(self.config.options) + args["cipher_list"] = ciphers_server self.server_conn.establish_tls( - self.config.client_certs, - self.server_sni, - method=self.config.openssl_method_server, - options=self.config.openssl_options_server, - verify=self.config.openssl_verification_mode_server, - ca_path=self.config.options.ssl_verify_upstream_trusted_cadir, - ca_pemfile=self.config.options.ssl_verify_upstream_trusted_ca, - cipher_list=ciphers_server, + sni=self.server_sni, alpn_protos=alpn, + **args ) tls_cert_err = self.server_conn.ssl_verification_error if tls_cert_err is not None: diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index e2088e71..566c42e6 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -47,7 +47,7 @@ CompletionState = typing.NamedTuple( ) -class CommandBuffer(): +class CommandBuffer: def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None: self.master = master self.text = self.flatten(start) diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 5907fe95..03f2e240 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -277,6 +277,17 @@ class ConsoleAddon: """ signals.status_prompt_command.send(partial=" ".join(partial)) # type: ignore + @command.command("console.command.set") + def console_command_set(self, option: str) -> None: + """ + Prompt the user to set an option of the form "key[=value]". + """ + option_value = getattr(self.master.options, option, None) + current_value = option_value if option_value else "" + self.master.commands.call( + "console.command set %s=%s" % (option, current_value) + ) + @command.command("console.view.keybindings") def view_keybindings(self) -> None: """View the commands list.""" @@ -379,6 +390,8 @@ class ConsoleAddon: # but for now it is. if not flow: raise exceptions.CommandError("No flow selected.") + flow.backup() + require_dummy_response = ( part in ("response-headers", "response-body", "set-cookies") and flow.response is None diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index d01d9b7e..084ef262 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -26,8 +26,8 @@ def map(km): km.add("ctrl b", "console.nav.pageup", ["global"], "Page up") km.add("I", "console.intercept.toggle", ["global"], "Toggle intercept") - km.add("i", "console.command set intercept=", ["global"], "Set intercept") - km.add("W", "console.command set save_stream_file=", ["global"], "Stream to file") + km.add("i", "console.command.set intercept", ["global"], "Set intercept") + km.add("W", "console.command.set save_stream_file", ["global"], "Stream to file") km.add("A", "flow.resume @all", ["flowlist", "flowview"], "Resume all intercepted flows") km.add("a", "flow.resume @focus", ["flowlist", "flowview"], "Resume this intercepted flow") km.add( @@ -46,7 +46,7 @@ def map(km): ["flowlist", "flowview"], "Export this flow to file" ) - km.add("f", "console.command set view_filter=", ["flowlist"], "Set view filter") + km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter") km.add("F", "set console_focus_follow=toggle", ["flowlist"], "Set focus follow") km.add( "ctrl l", @@ -69,7 +69,7 @@ setup( 'h11>=0.7.0,<0.8', "h2>=3.0.1,<4", "hyperframe>=5.1.0,<6", - "kaitaistruct>=0.7, <0.8", + "kaitaistruct>=0.7,<0.9", "ldap3>=2.4,<2.5", "passlib>=1.6.5, <1.8", "pyasn1>=0.3.1,<0.5", @@ -99,7 +99,7 @@ setup( "rstcheck>=2.2, <4.0", "sphinx_rtd_theme>=0.1.9, <0.3", "sphinx-autobuild>=0.5.2, <0.8", - "sphinx>=1.3.5, <1.7", + "sphinx>=1.7,<1.8", "sphinxcontrib-documentedlist>=0.5.0, <0.7", "tox>=2.3, <3", ], diff --git a/test/mitmproxy/net/http/test_cookies.py b/test/mitmproxy/net/http/test_cookies.py index 77549d9e..e12b0f00 100644 --- a/test/mitmproxy/net/http/test_cookies.py +++ b/test/mitmproxy/net/http/test_cookies.py @@ -7,6 +7,10 @@ from mitmproxy.net.http import cookies cookie_pairs = [ [ + "=uno", + [["", "uno"]] + ], + [ "", [] ], @@ -16,7 +20,7 @@ cookie_pairs = [ ], [ "one", - [["one", None]] + [["one", ""]] ], [ "one=uno; two=due", @@ -36,7 +40,7 @@ cookie_pairs = [ ], [ "one=uno; two; three=tre", - [["one", "uno"], ["two", None], ["three", "tre"]] + [["one", "uno"], ["two", ""], ["three", "tre"]] ], [ "_lvs2=zHai1+Hq+Tc2vmc2r4GAbdOI5Jopg3EwsdUT9g=; " @@ -79,8 +83,12 @@ def test_read_quoted_string(): def test_read_cookie_pairs(): vals = [ [ + "=uno", + [["", "uno"]] + ], + [ "one", - [["one", None]] + [["one", ""]] ], [ "one=two", @@ -100,7 +108,7 @@ def test_read_cookie_pairs(): ], [ 'one="two"; three=four; five', - [["one", "two"], ["three", "four"], ["five", None]] + [["one", "two"], ["three", "four"], ["five", ""]] ], [ 'one="\\"two"; three=four', @@ -135,6 +143,12 @@ def test_cookie_roundtrips(): def test_parse_set_cookie_pairs(): pairs = [ [ + "=uno", + [[ + ["", "uno"] + ]] + ], + [ "one=uno", [[ ["one", "uno"] @@ -150,7 +164,7 @@ def test_parse_set_cookie_pairs(): "one=uno; foo", [[ ["one", "uno"], - ["foo", None] + ["foo", ""] ]] ], [ @@ -200,6 +214,12 @@ def test_parse_set_cookie_header(): ";", [] ], [ + "=uno", + [ + ("", "uno", ()) + ] + ], + [ "one=uno", [ ("one", "uno", ()) diff --git a/test/mitmproxy/net/http/test_response.py b/test/mitmproxy/net/http/test_response.py index af35bab3..f3470384 100644 --- a/test/mitmproxy/net/http/test_response.py +++ b/test/mitmproxy/net/http/test_response.py @@ -113,7 +113,7 @@ class TestResponseUtils: assert attrs["domain"] == "example.com" assert attrs["expires"] == "Wed Oct 21 16:29:41 2015" assert attrs["path"] == "/" - assert attrs["httponly"] is None + assert attrs["httponly"] == "" def test_get_cookies_no_value(self): resp = tresp() diff --git a/test/mitmproxy/test_connections.py b/test/mitmproxy/test_connections.py index 9e5d89f1..00cdbc87 100644 --- a/test/mitmproxy/test_connections.py +++ b/test/mitmproxy/test_connections.py @@ -155,7 +155,7 @@ class TestServerConnection: def test_sni(self): c = connections.ServerConnection(('', 1234)) with pytest.raises(ValueError, matches='sni must be str, not '): - c.establish_tls(None, b'foobar') + c.establish_tls(sni=b'foobar') def test_state(self): c = tflow.tserver_conn() @@ -222,17 +222,16 @@ class TestServerConnectionTLS(tservers.ServerTestBase): def handle(self): self.finish() - @pytest.mark.parametrize("clientcert", [ + @pytest.mark.parametrize("client_certs", [ None, tutils.test_data.path("mitmproxy/data/clientcert"), tutils.test_data.path("mitmproxy/data/clientcert/client.pem"), ]) - def test_tls(self, clientcert): + def test_tls(self, client_certs): c = connections.ServerConnection(("127.0.0.1", self.port)) c.connect() - c.establish_tls(clientcert, "foo.com") + c.establish_tls(client_certs=client_certs) assert c.connected() - assert c.sni == "foo.com" assert c.tls_established c.close() c.finish() diff --git a/test/mitmproxy/tools/console/test_keymap.py b/test/mitmproxy/tools/console/test_keymap.py index 00e64991..7b475ff8 100644 --- a/test/mitmproxy/tools/console/test_keymap.py +++ b/test/mitmproxy/tools/console/test_keymap.py @@ -42,7 +42,7 @@ def test_join(): km = keymap.Keymap(tctx.master) km.add("key", "str", ["options"], "help1") km.add("key", "str", ["commands"]) - return + assert len(km.bindings) == 1 assert len(km.bindings[0].contexts) == 2 assert km.bindings[0].help == "help1" diff --git a/web/package.json b/web/package.json index 31c2d6d6..77b13e8b 100644 --- a/web/package.json +++ b/web/package.json @@ -37,7 +37,8 @@ "redux-logger": "^3.0.6", "redux-mock-store": "^1.3.0", "redux-thunk": "^2.2.0", - "shallowequal": "^1.0.2" + "shallowequal": "^1.0.2", + "stable": "^0.1.6" }, "devDependencies": { "babel-core": "^6.26.0", diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx index 02a4fba1..e60ed487 100644 --- a/web/src/js/components/FlowTable/FlowColumns.jsx +++ b/web/src/js/components/FlowTable/FlowColumns.jsx @@ -119,7 +119,7 @@ export function TimeColumn({ flow }) { return ( <td className="col-time"> {flow.response ? ( - formatTimeDelta(1000 * (flow.response.timestamp_end - flow.server_conn.timestamp_start)) + formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)) ) : ( '...' )} diff --git a/web/src/js/ducks/utils/store.js b/web/src/js/ducks/utils/store.js index ac272650..ad2242ee 100644 --- a/web/src/js/ducks/utils/store.js +++ b/web/src/js/ducks/utils/store.js @@ -1,3 +1,5 @@ +import stable from 'stable' + export const SET_FILTER = 'LIST_SET_FILTER' export const SET_SORT = 'LIST_SET_SORT' export const ADD = 'LIST_ADD' @@ -35,7 +37,7 @@ export default function reduce(state = defaultState, action) { switch (action.type) { case SET_FILTER: - view = list.filter(action.filter).sort(action.sort) + view = stable(list.filter(action.filter), action.sort) viewIndex = {} view.forEach((item, index) => { viewIndex[item.id] = index @@ -43,7 +45,7 @@ export default function reduce(state = defaultState, action) { break case SET_SORT: - view = [...view].sort(action.sort) + view = stable([...view], action.sort) viewIndex = {} view.forEach((item, index) => { viewIndex[item.id] = index diff --git a/web/yarn.lock b/web/yarn.lock index aa5ae85f..1930fded 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -5449,6 +5449,10 @@ sshpk@^1.7.0: jsbn "~0.1.0"
tweetnacl "~0.14.0"
+stable@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.6.tgz#910f5d2aed7b520c6e777499c1f32e139fdecb10"
+
statuses@1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|