diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6b9d0c0d..ffd33c525 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,7 @@ Unreleased: mitmproxy next * Updated imports and styles for web scanner helper addons. (@anneborcherding) * Inform when underscore-formatted options are used in client arg. (@jrblixt) * Binaries are now built with Python 3.9 (@mhils) +* Fixed the web UI showing blank page on clicking details tab when server address is missing (@samhita-sopho) * --- TODO: add new PRs above this line --- diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index a0c952418..472670f03 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -23,7 +23,7 @@ from mitmproxy import ctx from mitmproxy import io from mitmproxy import http from mitmproxy import tcp -from mitmproxy.utils import human +from mitmproxy.utils import compat, human # The underlying sorted list implementation expects the sort key to be stable @@ -460,8 +460,12 @@ class View(collections.abc.Sequence): req = http.HTTPRequest.make(method.upper(), url) except ValueError as e: raise exceptions.CommandError("Invalid URL: %s" % e) - c = connections.ClientConnection.make_dummy(("", 0)) - s = connections.ServerConnection.make_dummy((req.host, req.port)) + if compat.new_proxy_core: # pragma: no cover + c = compat.Client(("", 0), ("", 0), req.timestamp_start - 0.0001) + s = compat.Server((req.host, req.port)) + else: # pragma: no cover + c = connections.ClientConnection.make_dummy(("", 0)) + s = connections.ServerConnection.make_dummy((req.host, req.port)) f = http.HTTPFlow(c, s) f.request = req f.request.headers["Host"] = req.host diff --git a/mitmproxy/connections.py b/mitmproxy/connections.py index ab76aa6f6..ff105df0e 100644 --- a/mitmproxy/connections.py +++ b/mitmproxy/connections.py @@ -86,6 +86,15 @@ class ClientConnection(tcp.BaseHandler, stateobject.StateObject): def __hash__(self): return hash(self.id) + # Sans-io attributes. + state = 0 + sockname = ("", 0) + error = None + tls = None + certificate_list = None + alpn_offers = None + cipher_list = None + _stateobject_attributes = dict( id=str, address=tuple, @@ -100,6 +109,14 @@ class ClientConnection(tcp.BaseHandler, stateobject.StateObject): alpn_proto_negotiated=bytes, tls_version=str, tls_extensions=typing.List[typing.Tuple[int, bytes]], + # sans-io exclusives + state=int, + sockname=tuple, + error=str, + tls=bool, + certificate_list=typing.List[certs.Cert], + alpn_offers=typing.List[bytes], + cipher_list=typing.List[str], ) def send(self, message): @@ -130,6 +147,13 @@ class ClientConnection(tcp.BaseHandler, stateobject.StateObject): alpn_proto_negotiated=None, tls_version=None, tls_extensions=None, + state=0, + sockname=("", 0), + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_list=[], )) def convert_to_tls(self, cert, *args, **kwargs): @@ -221,6 +245,16 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): def __hash__(self): return hash(self.id) + # Sans-io attributes. + state = 0 + error = None + tls = None + certificate_list = None + alpn_offers = None + cipher_name = None + cipher_list = None + via2 = None + _stateobject_attributes = dict( id=str, address=tuple, @@ -235,6 +269,15 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): timestamp_tcp_setup=float, timestamp_tls_setup=float, timestamp_end=float, + # sans-io exclusives + state=int, + error=str, + tls=bool, + certificate_list=typing.List[certs.Cert], + alpn_offers=typing.List[bytes], + cipher_name=str, + cipher_list=typing.List[str], + via2=None, ) @classmethod @@ -259,7 +302,15 @@ class ServerConnection(tcp.TCPClient, stateobject.StateObject): timestamp_tcp_setup=None, timestamp_tls_setup=None, timestamp_end=None, - via=None + via=None, + state=0, + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_name=None, + cipher_list=[], + via2=None, )) def connect(self): diff --git a/mitmproxy/io/compat.py b/mitmproxy/io/compat.py index 142daba16..a2ba226b9 100644 --- a/mitmproxy/io/compat.py +++ b/mitmproxy/io/compat.py @@ -197,6 +197,39 @@ def convert_8_9(data): return data +def convert_9_10(data): + data["version"] = 10 + + def conv_conn(conn): + conn["state"] = 0 + conn["error"] = None + conn["tls"] = conn["tls_established"] + alpn = conn["alpn_proto_negotiated"] + conn["alpn_offers"] = [alpn] if alpn else None + cipher = conn["cipher_name"] + conn["cipher_list"] = [cipher] if cipher else None + + def conv_cconn(conn): + conn["sockname"] = ("", 0) + cc = conn["clientcert"] + conn["certificate_list"] = [cc] if cc else None + conv_conn(conn) + + def conv_sconn(conn): + crt = conn["cert"] + conn["certificate_list"] = [crt] if crt else None + conn["cipher_name"] = None + conn["via2"] = None + conv_conn(conn) + + conv_cconn(data["client_conn"]) + conv_sconn(data["server_conn"]) + if data["server_conn"]["via"]: + conv_sconn(data["server_conn"]["via"]) + + return data + + def _convert_dict_keys(o: Any) -> Any: if isinstance(o, dict): return {strutils.always_str(k): _convert_dict_keys(v) for k, v in o.items()} @@ -253,6 +286,7 @@ converters = { 6: convert_6_7, 7: convert_7_8, 8: convert_8_9, + 9: convert_9_10, } diff --git a/mitmproxy/test/tflow.py b/mitmproxy/test/tflow.py index aed38987e..7933e277e 100644 --- a/mitmproxy/test/tflow.py +++ b/mitmproxy/test/tflow.py @@ -165,6 +165,13 @@ def tclient_conn(): alpn_proto_negotiated=b"http/1.1", tls_version="TLSv1.2", tls_extensions=[(0x00, bytes.fromhex("000e00000b6578616d"))], + state=0, + sockname=("", 0), + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_list=[], )) c.reply = controller.DummyReply() c.rfile = io.BytesIO() @@ -191,6 +198,14 @@ def tserver_conn(): alpn_proto_negotiated=None, tls_version="TLSv1.2", via=None, + state=0, + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_name=None, + cipher_list=[], + via2=None, )) c.reply = controller.DummyReply() c.rfile = io.BytesIO() diff --git a/mitmproxy/tools/console/flowdetailview.py b/mitmproxy/tools/console/flowdetailview.py index fb2494e8b..d89078010 100644 --- a/mitmproxy/tools/console/flowdetailview.py +++ b/mitmproxy/tools/console/flowdetailview.py @@ -12,7 +12,12 @@ def maybe_timestamp(base, attr): if base is not None and getattr(base, attr): return human.format_timestamp_with_milli(getattr(base, attr)) else: - return "active" + # in mitmdump we serialize before a connection is closed. + # loading those flows at a later point shouldn't display "active". + # We also use a ndash (and not a regular dash) so that it is sorted + # after other timestamps. We may need to revisit that in the future if it turns out + # to render ugly in consoles. + return "–" def flowdetails(state, flow: mitmproxy.flow.Flow): diff --git a/mitmproxy/version.py b/mitmproxy/version.py index 7b90d4ee1..7d608249a 100644 --- a/mitmproxy/version.py +++ b/mitmproxy/version.py @@ -8,7 +8,7 @@ MITMPROXY = "mitmproxy " + VERSION # Serialization format version. This is displayed nowhere, it just needs to be incremented by one # for each change in the file format. -FLOW_FORMAT_VERSION = 9 +FLOW_FORMAT_VERSION = 10 def get_dev_version() -> str: diff --git a/web/src/js/__tests__/components/FlowView/DetailsSpec.js b/web/src/js/__tests__/components/FlowView/DetailsSpec.js index 1b0192cff..51be1ca72 100644 --- a/web/src/js/__tests__/components/FlowView/DetailsSpec.js +++ b/web/src/js/__tests__/components/FlowView/DetailsSpec.js @@ -47,4 +47,24 @@ describe('Details Component', () => { tree = details.toJSON() expect(tree).toMatchSnapshot() }) + + it('should render correctly when server address is missing', () => { + let tflowServerAddressNull = tflow + + tflowServerAddressNull.server_conn.address = null + tflowServerAddressNull.server_conn.ip_address = null + tflowServerAddressNull.server_conn.alpn_proto_negotiated = null + tflowServerAddressNull.server_conn.sni = null + tflowServerAddressNull.server_conn.ssl_established = false + tflowServerAddressNull.server_conn.tls_version = null + tflowServerAddressNull.server_conn.timestamp_tcp_setup = null + tflowServerAddressNull.server_conn.timestamp_ssl_setup = null + tflowServerAddressNull.server_conn.timestamp_start = null + tflowServerAddressNull.server_conn.timestamp_end = null + + let details = renderer.create(
), + tree = details.toJSON() + expect(tree).toMatchSnapshot() + }) + }) diff --git a/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap b/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap index 5b3bc748a..94b4cef9d 100644 --- a/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap +++ b/web/src/js/__tests__/components/FlowView/__snapshots__/DetailsSpec.js.snap @@ -237,6 +237,123 @@ exports[`Details Component should render correctly 1`] = ` `; +exports[`Details Component should render correctly when server address is missing 1`] = ` +
+

+ Client Connection +

+ + + + + + + + + + + + + + + + + + + + + + + +
+ Address: + + 127.0.0.1:22 +
+ + TLS SNI: + + + address +
+ TLS version: + + TLSv1.2 +
+ cipher name: + + cipher +
+ + ALPN: + + + http/1.1 +
+
+
+

+ Timing +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+ Client conn. established + : + + 1970-01-01 00:00:01.000 +
+ Client conn. SSL handshake + : + + 1970-01-01 00:00:02.000 +
+ First response byte + : + + 2017-05-21 12:38:32.481 +
+ Response complete + : + + 2017-05-21 12:38:32.481 +
+
+
+`; + exports[`TimeStamp Component should render correctly 1`] = ` diff --git a/web/src/js/components/FlowView/Details.jsx b/web/src/js/components/FlowView/Details.jsx index a09cf0d74..f9c615922 100644 --- a/web/src/js/components/FlowView/Details.jsx +++ b/web/src/js/components/FlowView/Details.jsx @@ -20,14 +20,14 @@ export function TimeStamp({ t, deltaTo, title }) { ) } -export function ConnectionInfo({ conn }) { +export function ConnectionInfo({ conn }) { return ( - + - + {conn.sni && ( @@ -136,9 +136,13 @@ export default function Details({ flow }) {

Client Connection

-

Server Connection

- - + {flow.server_conn.address && + [ +

Server Connection

, + + ] + } +
Address: {conn.address.join(':')}
TLS SNI: