def test_immediate_disconnect(self, tctx: context.Context, close_at): """Test the scenario where the client is disconnecting during the handshake. This may happen because they are not interested in the connection anymore, or because they do not like the proxy certificate.""" playbook, client_layer, tssl_client = make_client_tls_layer( tctx, sni=b"wrong.host.mitmproxy.org") playbook.logs = True playbook >> events.DataReceived(tctx.client, tssl_client.bio_read()) playbook << tls.TlsClienthelloHook(tutils.Placeholder()) if close_at == "tls_clienthello": assert (playbook >> events.ConnectionClosed( tctx.client) >> tutils.reply(to=-2) << tls.TlsStartClientHook( tutils.Placeholder()) >> reply_tls_start_client() << commands.CloseConnection(tctx.client)) return playbook >> tutils.reply() playbook << tls.TlsStartClientHook(tutils.Placeholder()) if close_at == "tls_start_client": assert (playbook >> events.ConnectionClosed(tctx.client) >> reply_tls_start_client(to=-2) << commands.CloseConnection( tctx.client)) return assert (playbook >> reply_tls_start_client() << commands.SendData( tctx.client, tutils.Placeholder() ) >> events.ConnectionClosed(tctx.client) << commands.Log( "Client TLS handshake failed. The client disconnected during the handshake. " "If this happens consistently for wrong.host.mitmproxy.org, this may indicate that the " "client does not trust the proxy's certificate.", "info") << commands.CloseConnection(tctx.client))
def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: if data: self.tls.bio_write(data) yield from self.tls_interact() plaintext = bytearray() close = False while True: try: plaintext.extend(self.tls.recv(65535)) except SSL.WantReadError: break except SSL.ZeroReturnError: close = True break if plaintext: yield from self.event_to_child( events.DataReceived(self.conn, bytes(plaintext))) if close: self.conn.state &= ~context.ConnectionState.CAN_READ if self.debug: yield commands.Log( f"{self.debug}[tls] close_notify {self.conn}", level="debug") yield from self.event_to_child(events.ConnectionClosed(self.conn))
def test_mitmproxy_ca_is_untrusted(self, tctx: context.Context): """Test the scenario where the client doesn't trust the mitmproxy CA.""" playbook, client_layer, tssl_client = make_client_tls_layer(tctx, sni=b"wrong.host.mitmproxy.org") playbook.logs = True data = tutils.Placeholder(bytes) assert ( playbook >> events.DataReceived(tctx.client, tssl_client.bio_read()) << tls.TlsClienthelloHook(tutils.Placeholder()) >> tutils.reply() << tls.TlsStartHook(tutils.Placeholder()) >> reply_tls_start() << commands.SendData(tctx.client, data) ) tssl_client.bio_write(data()) with pytest.raises(ssl.SSLCertVerificationError): tssl_client.do_handshake() # Finish Handshake assert ( playbook >> events.DataReceived(tctx.client, tssl_client.bio_read()) << commands.Log("Client TLS handshake failed. The client does not trust the proxy's certificate " "for wrong.host.mitmproxy.org (sslv3 alert bad certificate)", "warn") << commands.CloseConnection(tctx.client) >> events.ConnectionClosed(tctx.client) ) assert not tctx.client.tls_established
def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: if data: self.tls.bio_write(data) yield from self.tls_interact() plaintext = bytearray() close = False while True: try: plaintext.extend(self.tls.recv(65535)) except SSL.WantReadError: break except SSL.ZeroReturnError: close = True break except SSL.Error as e: # This may be happening because the other side send an alert. # There's somewhat ugly behavior with Firefox on Android here, # which upon mistrusting a certificate still completes the handshake # and then sends an alert in the next packet. At this point we have unfortunately # already fired out `tls_established_client` hook. yield commands.Log(f"TLS Error: {e}", "warn") break if plaintext: yield from self.event_to_child( events.DataReceived(self.conn, bytes(plaintext))) if close: self.conn.state &= ~connection.ConnectionState.CAN_READ if self.debug: yield commands.Log( f"{self.debug}[tls] close_notify {self.conn}", level="debug") yield from self.event_to_child(events.ConnectionClosed(self.conn))
def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: assert self.flow.response assert self.child_layer # HTTP events -> normal connection events if isinstance(event, RequestData): event = events.DataReceived(self.context.client, event.data) elif isinstance(event, ResponseData): event = events.DataReceived(self.context.server, event.data) elif isinstance(event, RequestEndOfMessage): event = events.ConnectionClosed(self.context.client) elif isinstance(event, ResponseEndOfMessage): event = events.ConnectionClosed(self.context.server) for command in self.child_layer.handle_event(event): # normal connection events -> HTTP events if isinstance(command, commands.SendData): if command.connection == self.context.client: yield SendHttp(ResponseData(self.stream_id, command.data), self.context.client) elif command.connection == self.context.server and self.flow.response.status_code == 101: # there only is a HTTP server connection if we have switched protocols, # not if a connection is established via CONNECT. yield SendHttp(RequestData(self.stream_id, command.data), self.context.server) else: yield command elif isinstance(command, commands.CloseConnection): if command.connection == self.context.client: yield SendHttp( ResponseProtocolError(self.stream_id, "EOF"), self.context.client) elif command.connection == self.context.server and self.flow.response.status_code == 101: yield SendHttp(RequestProtocolError(self.stream_id, "EOF"), self.context.server) else: # If we are running TCP over HTTP we want to be consistent with half-closes. # The easiest approach for this is to just always full close for now. # Alternatively, we could signal that we want a half close only through ResponseProtocolError, # but that is more complex to implement. command.half_close = False yield command else: yield command
def test_simple(self, tctx): playbook = tutils.Playbook(tls.ServerTLSLayer(tctx)) tctx.server.state = ConnectionState.OPEN tctx.server.address = ("example.mitmproxy.org", 443) tctx.server.sni = b"example.mitmproxy.org" tssl = SSLTest(server_side=True) # send ClientHello data = tutils.Placeholder(bytes) assert ( playbook << tls.TlsStartHook(tutils.Placeholder()) >> reply_tls_start() << commands.SendData(tctx.server, data) ) # receive ServerHello, finish client handshake tssl.bio_write(data()) with pytest.raises(ssl.SSLWantReadError): tssl.do_handshake() interact(playbook, tctx.server, tssl) # finish server handshake tssl.do_handshake() assert ( playbook >> events.DataReceived(tctx.server, tssl.bio_read()) << None ) assert tctx.server.tls_established # Echo assert ( playbook >> events.DataReceived(tctx.client, b"foo") << layer.NextLayerHook(tutils.Placeholder()) >> tutils.reply_next_layer(TlsEchoLayer) << commands.SendData(tctx.client, b"foo") ) _test_echo(playbook, tssl, tctx.server) with pytest.raises(ssl.SSLWantReadError): tssl.obj.unwrap() assert ( playbook >> events.DataReceived(tctx.server, tssl.bio_read()) << commands.CloseConnection(tctx.server) >> events.ConnectionClosed(tctx.server) << None )
async def handle_connection(self, connection: Connection) -> None: """ Handle a connection for its entire lifetime. This means we read until EOF, but then possibly also keep on waiting for our side of the connection to be closed. """ cancelled = None reader = self.transports[connection].reader assert reader while True: try: data = await reader.read(65535) if not data: raise OSError("Connection closed by peer.") except OSError: break except asyncio.CancelledError as e: cancelled = e break self.server_event(events.DataReceived(connection, data)) try: await self.drain_writers() except asyncio.CancelledError as e: cancelled = e break if cancelled is None: connection.state &= ~ConnectionState.CAN_READ else: connection.state = ConnectionState.CLOSED self.server_event(events.ConnectionClosed(connection)) if cancelled is None and connection.state is ConnectionState.CAN_WRITE: # we may still use this connection to *send* stuff, # even though the remote has closed their side of the connection. # to make this work we keep this task running and wait for cancellation. await asyncio.Event().wait() try: writer = self.transports[connection].writer assert writer writer.close() except OSError: pass self.transports.pop(connection) if cancelled: raise cancelled
def test_receive_close(self, tctx: Context, layer_found: bool): """Test that we abort a client connection which has disconnected without any layer being found.""" nl = layer.NextLayer(tctx) playbook = tutils.Playbook(nl) assert ( playbook >> events.DataReceived(tctx.client, b"foo") << layer.NextLayerHook(nl) >> events.ConnectionClosed(tctx.client)) if layer_found: nl.layer = tutils.RecordLayer(tctx) assert (playbook >> tutils.reply(to=-2)) assert isinstance(nl.layer.event_log[-1], events.ConnectionClosed) else: assert (playbook >> tutils.reply(to=-2) << commands.CloseConnection(tctx.client))
def test_mitmproxy_ca_is_untrusted_immediate_disconnect( self, tctx: context.Context): """Test the scenario where the client doesn't trust the mitmproxy CA.""" playbook, client_layer, tssl_client = make_client_tls_layer( tctx, sni=b"wrong.host.mitmproxy.org") assert ( playbook >> events.DataReceived( tctx.client, tssl_client.bio_read()) << tls.TlsClienthelloHook( tutils.Placeholder()) >> tutils.reply() << tls.TlsStartHook(tutils.Placeholder()) >> reply_tls_start() << commands.SendData(tctx.client, tutils.Placeholder()) >> events.ConnectionClosed(tctx.client) << commands.Log( "Client TLS handshake failed. The client may not trust the proxy's certificate " "for wrong.host.mitmproxy.org (connection closed)", "warn") << commands.CloseConnection(tctx.client))
def receive_close(self) -> layer.CommandGenerator[None]: yield from self.event_to_child(events.ConnectionClosed(self.conn))
def test_dataclasses(tconn): assert repr(events.Start()) assert repr(events.DataReceived(tconn, b"foo")) assert repr(events.ConnectionClosed(tconn))