def test_cannot_parse_clienthello(self, tctx: context.Context): """Test the scenario where we cannot parse the ClientHello""" playbook, client_layer, tssl_client = make_client_tls_layer(tctx) tls_hook_data = tutils.Placeholder(TlsData) invalid = b"\x16\x03\x01\x00\x00" assert (playbook >> events.DataReceived( tctx.client, invalid ) << commands.Log( f"Client TLS handshake failed. Cannot parse ClientHello: {invalid.hex()}", level="warn") << tls.TlsFailedClientHook(tls_hook_data) >> tutils.reply() << commands.CloseConnection(tctx.client)) assert tls_hook_data().conn.error assert not tctx.client.tls_established # Make sure that an active server connection does not cause child layers to spawn. client_layer.debug = "" assert (playbook >> events.DataReceived( Server(None), b"data on other stream" ) << commands.Log( ">> DataReceived(server, b'data on other stream')", 'debug' ) << commands.Log( "Swallowing DataReceived(server, b'data on other stream') as handshake failed.", "debug"))
def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived): request_head = self.buf.maybe_extract_lines() if request_head: request_head = [bytes(x) for x in request_head] # TODO: Make url.parse compatible with bytearrays try: self.request = http1.read_request_head(request_head) expected_body_size = http1.expected_http_body_size(self.request, expect_continue_as_0=False) except ValueError as e: yield commands.Log(f"{human.format_address(self.conn.peername)}: {e}") yield commands.CloseConnection(self.conn) self.state = self.done return yield ReceiveHttp(RequestHeaders(self.stream_id, self.request, expected_body_size == 0)) self.body_reader = make_body_reader(expected_body_size) self.state = self.read_body yield from self.state(event) else: pass # FIXME: protect against header size DoS elif isinstance(event, events.ConnectionClosed): buf = bytes(self.buf) if buf.strip(): yield commands.Log(f"Client closed connection before completing request headers: {buf!r}") yield commands.CloseConnection(self.conn) else: raise AssertionError(f"Unexpected event: {event}")
def receive_handshake_data( self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: if not self.send_connect: return (yield from super().receive_handshake_data(data)) self.buf += data response_head = self.buf.maybe_extract_lines() if response_head: response_head = [ bytes(x) for x in response_head ] # TODO: Make url.parse compatible with bytearrays try: response = http1.read_response_head(response_head) except ValueError as e: yield commands.Log( f"{human.format_address(self.tunnel_connection.address)}: {e}" ) return False, str(e) if 200 <= response.status_code < 300: if self.buf: yield from self.receive_data(bytes(self.buf)) del self.buf return True, None else: raw_resp = b"\n".join(response_head) yield commands.Log( f"{human.format_address(self.tunnel_connection.address)}: {raw_resp!r}", level="debug") return False, f"{response.status_code} {response.reason}" else: return False, None
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 send_response(self, already_streamed: bool = False): """We have either consumed the entire response from the server or the response was set by an addon.""" assert self.flow.response self.flow.response.timestamp_end = time.time() is_websocket = (self.flow.response.status_code == 101 and self.flow.response.headers.get( "upgrade", "").lower() == "websocket" and self.flow.request.headers.get( "Sec-WebSocket-Version", "").encode() == wsproto.handshake.WEBSOCKET_VERSION and self.context.options.websocket) if is_websocket: # We need to set this before calling the response hook # so that addons can determine if a WebSocket connection is following up. self.flow.websocket = WebSocketData() yield HttpResponseHook(self.flow) self.server_state = self.state_done if (yield from self.check_killed(False)): return if not already_streamed: content = self.flow.response.raw_content done_after_headers = not (content or self.flow.response.trailers) yield SendHttp( ResponseHeaders(self.stream_id, self.flow.response, done_after_headers), self.context.client) if content: yield SendHttp(ResponseData(self.stream_id, content), self.context.client) if self.flow.response.trailers: yield SendHttp( ResponseTrailers(self.stream_id, self.flow.response.trailers), self.context.client) yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) if self.flow.response.status_code == 101: if is_websocket: self.child_layer = websocket.WebsocketLayer( self.context, self.flow) elif self.context.options.rawtcp: self.child_layer = tcp.TCPLayer(self.context) else: yield commands.Log( f"Sent HTTP 101 response, but no protocol is enabled to upgrade to.", "warn") yield commands.CloseConnection(self.context.client) self.client_state = self.server_state = self.state_errored return if self.debug: yield commands.Log( f"{self.debug}[http] upgrading to {self.child_layer}", "debug") yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough return
def read_headers( self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived): request_head = self.buf.maybe_extract_lines() if request_head: request_head = [ bytes(x) for x in request_head ] # TODO: Make url.parse compatible with bytearrays try: self.request = http1.read_request_head(request_head) if self.context.options.validate_inbound_headers: http1.validate_headers(self.request.headers) expected_body_size = http1.expected_http_body_size( self.request) except ValueError as e: yield commands.SendData(self.conn, make_error_response(400, str(e))) yield commands.CloseConnection(self.conn) if self.request: # we have headers that we can show in the ui yield ReceiveHttp( RequestHeaders(self.stream_id, self.request, False)) yield ReceiveHttp( RequestProtocolError(self.stream_id, str(e), 400)) else: yield commands.Log( f"{human.format_address(self.conn.peername)}: {e}") self.state = self.done return yield ReceiveHttp( RequestHeaders(self.stream_id, self.request, expected_body_size == 0)) self.body_reader = make_body_reader(expected_body_size) self.state = self.read_body yield from self.state(event) else: pass # FIXME: protect against header size DoS elif isinstance(event, events.ConnectionClosed): buf = bytes(self.buf) if buf.strip(): yield commands.Log( f"Client closed connection before completing request headers: {buf!r}" ) yield commands.CloseConnection(self.conn) else: raise AssertionError(f"Unexpected event: {event}")
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 test_unsupported_protocol(self, tctx: context.Context): """Test the scenario where the server only supports an outdated TLS version by default.""" playbook = tutils.Playbook(tls.ServerTLSLayer(tctx)) tctx.server.address = ("example.mitmproxy.org", 443) tctx.server.state = ConnectionState.OPEN tctx.server.sni = "example.mitmproxy.org" # noinspection PyTypeChecker tssl = SSLTest(server_side=True, max_ver=ssl.TLSVersion.TLSv1_2) # send ClientHello data = tutils.Placeholder(bytes) assert ( playbook << tls.TlsStartServerHook(tutils.Placeholder()) >> reply_tls_start_server() << commands.SendData(tctx.server, data)) # receive ServerHello tssl.bio_write(data()) with pytest.raises(ssl.SSLError): tssl.do_handshake() # send back error tls_hook_data = tutils.Placeholder(TlsData) assert (playbook >> events.DataReceived(tctx.server, tssl.bio_read( )) << commands.Log( "Server TLS handshake failed. The remote server and mitmproxy cannot agree on a TLS version" " to use. You may need to adjust mitmproxy's tls_version_server_min option.", "warn") << tls.TlsFailedServerHook(tls_hook_data) >> tutils.reply() << commands.CloseConnection(tctx.server)) assert tls_hook_data().conn.error
def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: if self.conn.sni: dest = self.conn.sni else: dest = human.format_address(self.context.server.address) level: Literal["warn", "info"] = "warn" if err.startswith("Cannot parse ClientHello"): pass elif "unknown ca" in err or "bad certificate" in err: err = f"The client does not trust the proxy's certificate for {dest} ({err})" elif err == "connection closed": err = ( f"The client disconnected during the handshake. If this happens consistently for {dest}, " f"this may indicate that the client does not trust the proxy's certificate." ) level = "info" elif err == "connection closed early": pass else: err = f"The client may not trust the proxy's certificate for {dest} ({err})" if err != "connection closed early": yield commands.Log(f"Client TLS handshake failed. {err}", level=level) yield from super().on_handshake_error(err) self.event_to_child = self.errored # type: ignore
def send_response(self, already_streamed: bool = False): """We have either consumed the entire response from the server or the response was set by an addon.""" assert self.flow.response self.flow.response.timestamp_end = time.time() yield HttpResponseHook(self.flow) self.server_state = self.state_done if (yield from self.check_killed(False)): return if not already_streamed: content = self.flow.response.raw_content yield SendHttp(ResponseHeaders(self.stream_id, self.flow.response, not content), self.context.client) if content: yield SendHttp(ResponseData(self.stream_id, content), self.context.client) yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) if self.flow.response.status_code == 101: is_websocket = ( self.flow.response.headers.get("upgrade", "").lower() == "websocket" and self.flow.request.headers.get("Sec-WebSocket-Version", "") == "13" ) if is_websocket: self.child_layer = websocket.WebsocketLayer(self.context, self.flow) else: self.child_layer = tcp.TCPLayer(self.context) if self.debug: yield commands.Log(f"{self.debug}[http] upgrading to {self.child_layer}", "debug") yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough return
def read_headers( self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived): if not self.request: # we just received some data for an unknown request. yield commands.Log( f"Unexpected data from server: {bytes(self.buf)!r}") yield commands.CloseConnection(self.conn) return assert self.stream_id response_head = self.buf.maybe_extract_lines() if response_head: response_head = [ bytes(x) for x in response_head ] # TODO: Make url.parse compatible with bytearrays try: self.response = http1.read_response_head(response_head) if self.context.options.validate_inbound_headers: http1.validate_headers(self.response.headers) expected_size = http1.expected_http_body_size( self.request, self.response) except ValueError as e: yield commands.CloseConnection(self.conn) yield ReceiveHttp( ResponseProtocolError( self.stream_id, f"Cannot parse HTTP response: {e}")) return yield ReceiveHttp( ResponseHeaders(self.stream_id, self.response, expected_size == 0)) self.body_reader = make_body_reader(expected_size) self.state = self.read_body yield from self.state(event) else: pass # FIXME: protect against header size DoS elif isinstance(event, events.ConnectionClosed): if self.conn.state & ConnectionState.CAN_WRITE: yield commands.CloseConnection(self.conn) if self.stream_id: if self.buf: yield ReceiveHttp( ResponseProtocolError( self.stream_id, f"unexpected server response: {bytes(self.buf)!r}") ) else: # The server has closed the connection to prevent us from continuing. # We need to signal that to the stream. # https://tools.ietf.org/html/rfc7231#section-6.5.11 yield ReceiveHttp( ResponseProtocolError(self.stream_id, "server closed connection")) else: return else: raise AssertionError(f"Unexpected event: {event}")
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 start(self, _) -> layer.CommandGenerator[None]: client_extensions = [] server_extensions = [] # Parse extension headers. We only support deflate at the moment and ignore everything else. assert self.flow.response # satisfy type checker ext_header = self.flow.response.headers.get("Sec-WebSocket-Extensions", "") if ext_header: for ext in wsproto.utilities.split_comma_header( ext_header.encode("ascii", "replace")): ext_name = ext.split(";", 1)[0].strip() if ext_name == wsproto.extensions.PerMessageDeflate.name: client_deflate = wsproto.extensions.PerMessageDeflate() client_deflate.finalize(ext) client_extensions.append(client_deflate) server_deflate = wsproto.extensions.PerMessageDeflate() server_deflate.finalize(ext) server_extensions.append(server_deflate) else: yield commands.Log( f"Ignoring unknown WebSocket extension {ext_name!r}.") self.client_ws = WebsocketConnection(wsproto.ConnectionType.SERVER, client_extensions, conn=self.context.client) self.server_ws = WebsocketConnection(wsproto.ConnectionType.CLIENT, server_extensions, conn=self.context.server) yield WebsocketStartHook(self.flow) self._handle_event = self.relay_messages
def _ask(self): """ Manually trigger a next_layer hook. The only use at the moment is to make sure that the top layer is initialized. """ yield NextLayerHook(self) # Has an addon decided on the next layer yet? if self.layer: if self.debug: yield commands.Log(f"{self.debug}[nextlayer] {self.layer!r}", "debug") for e in self.events: yield from self.layer.handle_event(e) self.events.clear() # Why do we need three assignments here? # 1. When this function here is invoked we may have paused events. Those should be # forwarded to the sublayer right away, so we reassign ._handle_event. # 2. This layer is not needed anymore, so we directly reassign .handle_event. # 3. Some layers may however still have a reference to the old .handle_event. # ._handle is just an optimization to reduce the callstack in these cases. self.handle_event = self.layer.handle_event self._handle_event = self.layer._handle_event self._handle = self.layer.handle_event
def receive_handshake_data( self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: # bio_write errors for b"", so we need to check first if we actually received something. if data: self.tls.bio_write(data) try: self.tls.do_handshake() except SSL.WantReadError: yield from self.tls_interact() return False, None except SSL.Error as e: # provide more detailed information for some errors. last_err = e.args and isinstance( e.args[0], list) and e.args[0] and e.args[0][-1] if last_err == ('SSL routines', 'tls_process_server_certificate', 'certificate verify failed'): verify_result = SSL._lib.SSL_get_verify_result(self.tls._ssl) error = SSL._ffi.string( SSL._lib.X509_verify_cert_error_string( verify_result)).decode() err = f"Certificate verify failed: {error}" elif last_err in [('SSL routines', 'ssl3_read_bytes', 'tlsv1 alert unknown ca'), ('SSL routines', 'ssl3_read_bytes', 'sslv3 alert bad certificate')]: assert isinstance(last_err, tuple) err = last_err[2] elif last_err == ('SSL routines', 'ssl3_get_record', 'wrong version number') and data[:4].isascii(): err = f"The remote server does not speak TLS." else: # pragma: no cover # TODO: Add test case one we find one. err = f"OpenSSL {e!r}" return False, err else: # Here we set all attributes that are only known *after* the handshake. # Get all peer certificates. # https://www.openssl.org/docs/man1.1.1/man3/SSL_get_peer_cert_chain.html # If called on the client side, the stack also contains the peer's certificate; if called on the server # side, the peer's certificate must be obtained separately using SSL_get_peer_certificate(3). all_certs = self.tls.get_peer_cert_chain() or [] if self.conn == self.context.client: cert = self.tls.get_peer_certificate() if cert: all_certs.insert(0, cert) self.conn.timestamp_tls_setup = time.time() self.conn.alpn = self.tls.get_alpn_proto_negotiated() self.conn.certificate_list = [ certs.Cert.from_pyopenssl(x) for x in all_certs ] self.conn.cipher = self.tls.get_cipher_name() self.conn.tls_version = self.tls.get_protocol_version_name() if self.debug: yield commands.Log( f"{self.debug}[tls] tls established: {self.conn}", "debug") yield from self.receive_data(b"") return True, None
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 on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: if self.conn.sni: dest = self.conn.sni else: dest = human.format_address(self.context.server.address) level: Literal["warn", "info"] = "warn" if err.startswith("Cannot parse ClientHello"): pass elif "('SSL routines', 'tls_early_post_process_client_hello', 'unsupported protocol')" in err: err = ( f"Client and mitmproxy cannot agree on a TLS version to use. " f"You may need to adjust mitmproxy's tls_version_client_min option." ) elif "unknown ca" in err or "bad certificate" in err or "certificate unknown" in err: err = f"The client does not trust the proxy's certificate for {dest} ({err})" elif err == "connection closed": err = ( f"The client disconnected during the handshake. If this happens consistently for {dest}, " f"this may indicate that the client does not trust the proxy's certificate." ) level = "info" elif err == "connection closed early": pass else: err = f"The client may not trust the proxy's certificate for {dest} ({err})" if err != "connection closed early": yield commands.Log(f"Client TLS handshake failed. {err}", level=level) yield from super().on_handshake_error(err) self.event_to_child = self.errored # type: ignore
def test_untrusted_cert(self, tctx): """If the certificate is not trusted, we should fail.""" playbook = tutils.Playbook(tls.ServerTLSLayer(tctx)) tctx.server.address = ("wrong.host.mitmproxy.org", 443) tctx.server.sni = "wrong.host.mitmproxy.org" tssl = SSLTest(server_side=True) # send ClientHello data = tutils.Placeholder(bytes) assert (playbook >> events.DataReceived( tctx.client, b"open-connection") << layer.NextLayerHook( tutils.Placeholder()) >> tutils.reply_next_layer(TlsEchoLayer) << commands.OpenConnection(tctx.server) >> tutils.reply(None) << 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() assert (playbook >> events.DataReceived(tctx.server, tssl.bio_read( )) << commands.Log( "Server TLS handshake failed. Certificate verify failed: Hostname mismatch", "warn" ) << commands.CloseConnection(tctx.server) << commands.SendData( tctx.client, b"open-connection failed: Certificate verify failed: Hostname mismatch" )) assert not tctx.server.tls_established
def receive_handshake_data( self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: if self.client_hello_parsed: return (yield from super().receive_handshake_data(data)) self.recv_buffer.extend(data) try: client_hello = parse_client_hello(self.recv_buffer) except ValueError: return False, f"Cannot parse ClientHello: {self.recv_buffer.hex()}" if client_hello: self.client_hello_parsed = True else: return False, None self.conn.sni = client_hello.sni self.conn.alpn_offers = client_hello.alpn_protocols tls_clienthello = ClientHelloData(self.context) yield TlsClienthelloHook(tls_clienthello) if tls_clienthello.establish_server_tls_first and not self.context.server.tls_established: err = yield from self.start_server_tls() if err: yield commands.Log( f"Unable to establish TLS connection with server ({err}). " f"Trying to establish TLS with client anyway.") yield from self.start_tls() ret = yield from super().receive_handshake_data(bytes( self.recv_buffer)) self.recv_buffer.clear() return ret
def start_response_stream(self) -> layer.CommandGenerator[None]: assert self.flow.response yield SendHttp( ResponseHeaders(self.stream_id, self.flow.response, end_stream=False), self.context.client) yield commands.Log( f"Streaming response from {self.flow.request.host}.") self.server_state = self.state_stream_response_body
def __debug(self, message): if len(message) > 512: message = message[:512] + "…" if Layer.__last_debug_message == message: message = message.split("\n", 1)[0].strip() if len(message) > 256: message = message[:256] + "…" else: Layer.__last_debug_message = message return commands.Log(textwrap.indent(message, self.debug), "debug")
def start_tls(self) -> layer.CommandGenerator[None]: assert not self.tls tls_start = TlsStartData(self.conn, self.context) yield TlsStartHook(tls_start) if not tls_start.ssl_conn: yield commands.Log("No TLS context was provided, failing connection.", "error") yield commands.CloseConnection(self.conn) assert tls_start.ssl_conn self.tls = tls_start.ssl_conn
def __debug(self, message): """yield a Log command indicating what message is passing through this layer.""" if len(message) > 512: message = message[:512] + "…" if Layer.__last_debug_message == message: message = message.split("\n", 1)[0].strip() if len(message) > 256: message = message[:256] + "…" else: Layer.__last_debug_message = message return commands.Log(textwrap.indent(message, self.debug), "debug")
def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: assert platform.original_addr is not None socket = yield commands.GetSocket(self.context.client) try: self.context.server.address = platform.original_addr(socket) except Exception as e: yield commands.Log(f"Transparent mode failure: {e!r}") self.child_layer = layer.NextLayer(self.context) yield from self.finish_start()
def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, events.Start): pass elif isinstance(event, events.DataReceived): self.buf += event.data yield from self.state() elif isinstance(event, events.ConnectionClosed): if self.buf: yield commands.Log(f"Client closed connection before completing SOCKS5 handshake: {self.buf!r}") yield commands.CloseConnection(event.connection) else: raise AssertionError(f"Unknown event: {event}")
def start_request_stream(self) -> layer.CommandGenerator[None]: if self.flow.response: raise NotImplementedError( "Can't set a response and enable streaming at the same time.") ok = yield from self.make_server_connection() if not ok: return yield SendHttp( RequestHeaders(self.stream_id, self.flow.request, end_stream=False), self.context.server) yield commands.Log(f"Streaming request to {self.flow.request.host}.") self.client_state = self.state_stream_request_body
def test_cannot_parse_clienthello(self, tctx: context.Context): """Test the scenario where we cannot parse the ClientHello""" playbook, client_layer, tssl_client = make_client_tls_layer(tctx) invalid = b"\x16\x03\x01\x00\x00" assert (playbook >> events.DataReceived( tctx.client, invalid ) << commands.Log( f"Client TLS handshake failed. Cannot parse ClientHello: {invalid.hex()}", level="warn") << commands.CloseConnection(tctx.client)) assert not tctx.client.tls_established
def receive_handshake_data( self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: if self.client_hello_parsed: return (yield from super().receive_handshake_data(data)) self.recv_buffer.extend(data) try: client_hello = parse_client_hello(self.recv_buffer) except ValueError: return False, f"Cannot parse ClientHello: {self.recv_buffer.hex()}" if client_hello: self.client_hello_parsed = True else: return False, None self.conn.sni = client_hello.sni self.conn.alpn_offers = client_hello.alpn_protocols tls_clienthello = ClientHelloData(self.context, client_hello) yield TlsClienthelloHook(tls_clienthello) if tls_clienthello.ignore_connection: # we've figured out that we don't want to intercept this connection, so we assign fake connection objects # to all TLS layers. This makes the real connection contents just go through. self.conn = self.tunnel_connection = connection.Client( ("ignore-conn", 0), ("ignore-conn", 0), time.time()) parent_layer = self.context.layers[self.context.layers.index(self) - 1] if isinstance(parent_layer, ServerTLSLayer): parent_layer.conn = parent_layer.tunnel_connection = connection.Server( None) self.child_layer = tcp.TCPLayer(self.context, ignore=True) yield from self.event_to_child( events.DataReceived(self.context.client, bytes(self.recv_buffer))) self.recv_buffer.clear() return True, None if tls_clienthello.establish_server_tls_first and not self.context.server.tls_established: err = yield from self.start_server_tls() if err: yield commands.Log( f"Unable to establish TLS connection with server ({err}). " f"Trying to establish TLS with client anyway.") yield from self.start_tls() if not self.conn.connected: return False, "connection closed early" ret = yield from super().receive_handshake_data(bytes( self.recv_buffer)) self.recv_buffer.clear() return ret
def socks_err( self, message: str, reply_code: Optional[int] = None, ) -> layer.CommandGenerator[None]: if reply_code is not None: yield commands.SendData( self.context.client, bytes([SOCKS5_VERSION, reply_code]) + b"\x00\x01\x00\x00\x00\x00\x00\x00") yield commands.CloseConnection(self.context.client) yield commands.Log(message) self._handle_event = self.done
def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: if self.conn.sni: dest = self.conn.sni else: dest = human.format_address(self.context.server.address) if err.startswith("Cannot parse ClientHello"): pass elif "unknown ca" in err or "bad certificate" in err: err = f"The client does not trust the proxy's certificate for {dest} ({err})" else: err = f"The client may not trust the proxy's certificate for {dest} ({err})" yield commands.Log(f"Client TLS handshake failed. {err}", level="warn") yield from super().on_handshake_error(err)