mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-26 18:18:25 +00:00
[sans-io] add tunnel tests
This commit is contained in:
parent
f0bdf887fc
commit
8f49cde627
@ -24,6 +24,11 @@ class TunnelLayer(layer.Layer):
|
|||||||
"""The 'inner' connection which provides data I/O"""
|
"""The 'inner' connection which provides data I/O"""
|
||||||
tunnel_state: TunnelState = TunnelState.INACTIVE
|
tunnel_state: TunnelState = TunnelState.INACTIVE
|
||||||
command_to_reply_to: Optional[commands.OpenConnection] = None
|
command_to_reply_to: Optional[commands.OpenConnection] = None
|
||||||
|
_event_queue: List[events.Event]
|
||||||
|
"""
|
||||||
|
If the connection already exists when we receive the start event,
|
||||||
|
we buffer commands until we have established the tunnel.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -35,6 +40,7 @@ class TunnelLayer(layer.Layer):
|
|||||||
self.tunnel_connection = tunnel_connection
|
self.tunnel_connection = tunnel_connection
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.child_layer = layer.NextLayer(self.context)
|
self.child_layer = layer.NextLayer(self.context)
|
||||||
|
self._event_queue = []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{type(self).__name__}({self.tunnel_state.name.lower()})"
|
return f"{type(self).__name__}({self.tunnel_state.name.lower()})"
|
||||||
@ -55,13 +61,12 @@ class TunnelLayer(layer.Layer):
|
|||||||
if done:
|
if done:
|
||||||
if self.conn != self.tunnel_connection:
|
if self.conn != self.tunnel_connection:
|
||||||
self.conn.state = context.ConnectionState.OPEN
|
self.conn.state = context.ConnectionState.OPEN
|
||||||
self.tunnel_state = TunnelState.OPEN
|
|
||||||
if err:
|
if err:
|
||||||
self.tunnel_state = TunnelState.CLOSED
|
if self.conn != self.tunnel_connection:
|
||||||
|
self.conn.state = context.ConnectionState.CLOSED
|
||||||
yield from self.on_handshake_error(err)
|
yield from self.on_handshake_error(err)
|
||||||
if (done or err) and self.command_to_reply_to:
|
if done or err:
|
||||||
yield from self.event_to_child(events.OpenConnectionReply(self.command_to_reply_to, err))
|
yield from self._handshake_finished(err)
|
||||||
self.command_to_reply_to = None
|
|
||||||
else:
|
else:
|
||||||
yield from self.receive_data(event.data)
|
yield from self.receive_data(event.data)
|
||||||
elif isinstance(event, events.ConnectionClosed):
|
elif isinstance(event, events.ConnectionClosed):
|
||||||
@ -69,13 +74,32 @@ class TunnelLayer(layer.Layer):
|
|||||||
if self.tunnel_state is TunnelState.OPEN:
|
if self.tunnel_state is TunnelState.OPEN:
|
||||||
yield from self.receive_close()
|
yield from self.receive_close()
|
||||||
elif self.tunnel_state is TunnelState.ESTABLISHING:
|
elif self.tunnel_state is TunnelState.ESTABLISHING:
|
||||||
yield from self.on_handshake_error("connection closed without notice")
|
err = "connection closed without notice"
|
||||||
else:
|
yield from self.on_handshake_error(err)
|
||||||
|
yield from self._handshake_finished(err)
|
||||||
|
self.tunnel_state = TunnelState.CLOSED
|
||||||
|
else: # pragma: no cover
|
||||||
raise AssertionError(f"Unexpected event: {event}")
|
raise AssertionError(f"Unexpected event: {event}")
|
||||||
else:
|
else:
|
||||||
yield from self.event_to_child(event)
|
yield from self.event_to_child(event)
|
||||||
|
|
||||||
|
def _handshake_finished(self, err: Optional[str]):
|
||||||
|
if err:
|
||||||
|
self.tunnel_state = TunnelState.CLOSED
|
||||||
|
else:
|
||||||
|
self.tunnel_state = TunnelState.OPEN
|
||||||
|
if self.command_to_reply_to:
|
||||||
|
yield from self.event_to_child(events.OpenConnectionReply(self.command_to_reply_to, err))
|
||||||
|
self.command_to_reply_to = None
|
||||||
|
else:
|
||||||
|
for evt in self._event_queue:
|
||||||
|
yield from self.event_to_child(evt)
|
||||||
|
self._event_queue.clear()
|
||||||
|
|
||||||
def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]:
|
def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]:
|
||||||
|
if self.tunnel_state is TunnelState.ESTABLISHING and not self.command_to_reply_to:
|
||||||
|
self._event_queue.append(event)
|
||||||
|
return
|
||||||
for command in self.child_layer.handle_event(event):
|
for command in self.child_layer.handle_event(event):
|
||||||
if isinstance(command, commands.ConnectionCommand) and command.connection == self.conn:
|
if isinstance(command, commands.ConnectionCommand) and command.connection == self.conn:
|
||||||
if isinstance(command, commands.SendData):
|
if isinstance(command, commands.SendData):
|
||||||
@ -89,14 +113,15 @@ class TunnelLayer(layer.Layer):
|
|||||||
yield from self.send_close(command.half_close)
|
yield from self.send_close(command.half_close)
|
||||||
elif isinstance(command, commands.OpenConnection):
|
elif isinstance(command, commands.OpenConnection):
|
||||||
# create our own OpenConnection command object that blocks here.
|
# create our own OpenConnection command object that blocks here.
|
||||||
|
self.command_to_reply_to = command
|
||||||
self.tunnel_state = TunnelState.ESTABLISHING
|
self.tunnel_state = TunnelState.ESTABLISHING
|
||||||
err = yield commands.OpenConnection(self.tunnel_connection)
|
err = yield commands.OpenConnection(self.tunnel_connection)
|
||||||
if err:
|
if err:
|
||||||
yield from self.event_to_child(events.OpenConnectionReply(command, err))
|
yield from self.event_to_child(events.OpenConnectionReply(command, err))
|
||||||
|
self.tunnel_state = TunnelState.CLOSED
|
||||||
else:
|
else:
|
||||||
self.command_to_reply_to = command
|
|
||||||
yield from self.start_handshake()
|
yield from self.start_handshake()
|
||||||
else:
|
else: # pragma: no cover
|
||||||
raise AssertionError(f"Unexpected command: {command}")
|
raise AssertionError(f"Unexpected command: {command}")
|
||||||
else:
|
else:
|
||||||
yield command
|
yield command
|
||||||
@ -105,6 +130,7 @@ class TunnelLayer(layer.Layer):
|
|||||||
yield from self._handle_event(events.DataReceived(self.tunnel_connection, b""))
|
yield from self._handle_event(events.DataReceived(self.tunnel_connection, b""))
|
||||||
|
|
||||||
def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]:
|
def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]:
|
||||||
|
"""returns a (done, err) tuple"""
|
||||||
yield from ()
|
yield from ()
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@ -142,14 +168,3 @@ class LayerStack:
|
|||||||
self._stack.append(other)
|
self._stack.append(other)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class OpenConnectionStub(layer.Layer):
|
|
||||||
done = False
|
|
||||||
err = None
|
|
||||||
|
|
||||||
def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]:
|
|
||||||
if isinstance(event, events.Start):
|
|
||||||
self.err = yield commands.OpenConnection(self.context.server)
|
|
||||||
self.done = not self.err
|
|
||||||
else:
|
|
||||||
self.err = event
|
|
||||||
|
@ -0,0 +1,262 @@
|
|||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mitmproxy.proxy2 import tunnel, layer
|
||||||
|
from mitmproxy.proxy2.commands import SendData, Log, CloseConnection, OpenConnection
|
||||||
|
from mitmproxy.proxy2.context import Context, Server, ConnectionState
|
||||||
|
from mitmproxy.proxy2.events import Event, DataReceived, Start, ConnectionClosed
|
||||||
|
from test.mitmproxy.proxy2.tutils import Playbook, reply
|
||||||
|
|
||||||
|
|
||||||
|
class TChildLayer(layer.Layer):
|
||||||
|
child_layer: Optional[layer.Layer] = None
|
||||||
|
|
||||||
|
def _handle_event(self, event: Event) -> layer.CommandGenerator[None]:
|
||||||
|
if isinstance(event, Start):
|
||||||
|
yield Log(f"Got start. Server state: {self.context.server.state.name}")
|
||||||
|
elif isinstance(event, DataReceived) and event.data == b"client-hello":
|
||||||
|
yield SendData(self.context.client, b"client-hello-reply")
|
||||||
|
elif isinstance(event, DataReceived) and event.data == b"server-hello":
|
||||||
|
yield SendData(self.context.server, b"server-hello-reply")
|
||||||
|
elif isinstance(event, DataReceived) and event.data == b"open":
|
||||||
|
err = yield OpenConnection(self.context.server)
|
||||||
|
yield Log(f"Opened: {err=}. Server state: {self.context.server.state.name}")
|
||||||
|
elif isinstance(event, DataReceived) and event.data == b"half-close":
|
||||||
|
err = yield CloseConnection(event.connection, half_close=True)
|
||||||
|
elif isinstance(event, ConnectionClosed):
|
||||||
|
yield Log(f"Got {event.connection.__class__.__name__.lower()} close.")
|
||||||
|
yield CloseConnection(event.connection)
|
||||||
|
else:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
|
||||||
|
class TTunnelLayer(tunnel.TunnelLayer):
|
||||||
|
def start_handshake(self) -> layer.CommandGenerator[None]:
|
||||||
|
yield SendData(self.tunnel_connection, b"handshake-hello")
|
||||||
|
|
||||||
|
def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]:
|
||||||
|
yield SendData(self.tunnel_connection, data)
|
||||||
|
if data == b"handshake-success":
|
||||||
|
return True, None
|
||||||
|
else:
|
||||||
|
return False, "handshake error"
|
||||||
|
|
||||||
|
def send_data(self, data: bytes) -> layer.CommandGenerator[None]:
|
||||||
|
yield SendData(self.tunnel_connection, b"tunneled-" + data)
|
||||||
|
|
||||||
|
def receive_data(self, data: bytes) -> layer.CommandGenerator[None]:
|
||||||
|
yield from self.event_to_child(
|
||||||
|
DataReceived(self.conn, data.replace(b"tunneled-", b""))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("success", ["success", "fail"])
|
||||||
|
def test_tunnel_handshake_start(tctx: Context, success):
|
||||||
|
server = Server(("proxy", 1234))
|
||||||
|
server.state = ConnectionState.OPEN
|
||||||
|
|
||||||
|
tl = TTunnelLayer(tctx, server, tctx.server)
|
||||||
|
tl.child_layer = TChildLayer(tctx)
|
||||||
|
assert repr(tl)
|
||||||
|
|
||||||
|
playbook = Playbook(tl, logs=True)
|
||||||
|
(
|
||||||
|
playbook
|
||||||
|
<< SendData(server, b"handshake-hello")
|
||||||
|
>> DataReceived(tctx.client, b"client-hello")
|
||||||
|
>> DataReceived(server, b"handshake-" + success.encode())
|
||||||
|
<< SendData(server, b"handshake-" + success.encode())
|
||||||
|
)
|
||||||
|
if success == "success":
|
||||||
|
playbook << Log("Got start. Server state: OPEN")
|
||||||
|
else:
|
||||||
|
playbook << CloseConnection(server)
|
||||||
|
playbook << Log("Got start. Server state: CLOSED")
|
||||||
|
|
||||||
|
playbook << SendData(tctx.client, b"client-hello-reply")
|
||||||
|
if success == "success":
|
||||||
|
playbook >> DataReceived(server, b"tunneled-server-hello")
|
||||||
|
playbook << SendData(server, b"tunneled-server-hello-reply")
|
||||||
|
|
||||||
|
assert playbook
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("success", ["success", "fail"])
|
||||||
|
def test_tunnel_handshake_command(tctx: Context, success):
|
||||||
|
server = Server(("proxy", 1234))
|
||||||
|
|
||||||
|
tl = TTunnelLayer(tctx, server, tctx.server)
|
||||||
|
tl.child_layer = TChildLayer(tctx)
|
||||||
|
|
||||||
|
playbook = Playbook(tl, logs=True)
|
||||||
|
(
|
||||||
|
playbook
|
||||||
|
<< Log("Got start. Server state: CLOSED")
|
||||||
|
>> DataReceived(tctx.client, b"client-hello")
|
||||||
|
<< SendData(tctx.client, b"client-hello-reply")
|
||||||
|
>> DataReceived(tctx.client, b"open")
|
||||||
|
<< OpenConnection(server)
|
||||||
|
>> reply(None)
|
||||||
|
<< SendData(server, b"handshake-hello")
|
||||||
|
>> DataReceived(server, b"handshake-" + success.encode())
|
||||||
|
<< SendData(server, b"handshake-" + success.encode())
|
||||||
|
)
|
||||||
|
if success == "success":
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
<< Log(f"Opened: err=None. Server state: OPEN")
|
||||||
|
>> DataReceived(server, b"tunneled-server-hello")
|
||||||
|
<< SendData(server, b"tunneled-server-hello-reply")
|
||||||
|
>> ConnectionClosed(tctx.client)
|
||||||
|
<< Log("Got client close.")
|
||||||
|
<< CloseConnection(tctx.client)
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.OPEN
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> ConnectionClosed(server)
|
||||||
|
<< Log("Got server close.")
|
||||||
|
<< CloseConnection(server)
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
|
||||||
|
else:
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
<< CloseConnection(server)
|
||||||
|
<< Log("Opened: err='handshake error'. Server state: CLOSED")
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
|
||||||
|
|
||||||
|
|
||||||
|
def test_tunnel_default_impls(tctx: Context):
|
||||||
|
tctx.server.state = ConnectionState.OPEN
|
||||||
|
tl = tunnel.TunnelLayer(tctx, tctx.server, Server(None))
|
||||||
|
tl.child_layer = TChildLayer(tctx)
|
||||||
|
playbook = Playbook(tl, logs=True)
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
<< Log("Got start. Server state: OPEN")
|
||||||
|
>> DataReceived(tctx.server, b"server-hello")
|
||||||
|
<< SendData(tctx.server, b"server-hello-reply")
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.OPEN
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> ConnectionClosed(tctx.server)
|
||||||
|
<< Log("Got server close.")
|
||||||
|
<< CloseConnection(tctx.server)
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
|
||||||
|
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> DataReceived(tctx.client, b"open")
|
||||||
|
<< OpenConnection(tctx.server)
|
||||||
|
>> reply(None)
|
||||||
|
<< Log("Opened: err=None. Server state: OPEN")
|
||||||
|
>> DataReceived(tctx.server, b"half-close")
|
||||||
|
<< CloseConnection(tctx.server, half_close=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tunnel_openconnection_error(tctx: Context):
|
||||||
|
server = Server(("proxy", 1234))
|
||||||
|
|
||||||
|
tl = TTunnelLayer(tctx, server, tctx.server)
|
||||||
|
tl.child_layer = TChildLayer(tctx)
|
||||||
|
|
||||||
|
playbook = Playbook(tl, logs=True)
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
<< Log("Got start. Server state: CLOSED")
|
||||||
|
>> DataReceived(tctx.client, b"open")
|
||||||
|
<< OpenConnection(server)
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.ESTABLISHING
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> reply("IPoAC packet dropped.")
|
||||||
|
<< Log("Opened: err='IPoAC packet dropped.'. Server state: CLOSED")
|
||||||
|
)
|
||||||
|
assert tl.tunnel_state is tunnel.TunnelState.CLOSED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("disconnect", ["client", "server"])
|
||||||
|
def test_disconnect_during_handshake_start(tctx: Context, disconnect):
|
||||||
|
server = Server(("proxy", 1234))
|
||||||
|
server.state = ConnectionState.OPEN
|
||||||
|
|
||||||
|
tl = TTunnelLayer(tctx, server, tctx.server)
|
||||||
|
tl.child_layer = TChildLayer(tctx)
|
||||||
|
|
||||||
|
playbook = Playbook(tl, logs=True)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
<< SendData(server, b"handshake-hello")
|
||||||
|
)
|
||||||
|
if disconnect == "client":
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> ConnectionClosed(tctx.client)
|
||||||
|
>> ConnectionClosed(server) # proxyserver will cancel all other connections as well.
|
||||||
|
<< CloseConnection(server)
|
||||||
|
<< Log("Got start. Server state: CLOSED")
|
||||||
|
<< Log("Got client close.")
|
||||||
|
<< CloseConnection(tctx.client)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> ConnectionClosed(server)
|
||||||
|
<< CloseConnection(server)
|
||||||
|
<< Log("Got start. Server state: CLOSED")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("disconnect", ["client", "server"])
|
||||||
|
def test_disconnect_during_handshake_command(tctx: Context, disconnect):
|
||||||
|
server = Server(("proxy", 1234))
|
||||||
|
|
||||||
|
tl = TTunnelLayer(tctx, server, tctx.server)
|
||||||
|
tl.child_layer = TChildLayer(tctx)
|
||||||
|
|
||||||
|
playbook = Playbook(tl, logs=True)
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
<< Log("Got start. Server state: CLOSED")
|
||||||
|
>> DataReceived(tctx.client, b"client-hello")
|
||||||
|
<< SendData(tctx.client, b"client-hello-reply")
|
||||||
|
>> DataReceived(tctx.client, b"open")
|
||||||
|
<< OpenConnection(server)
|
||||||
|
>> reply(None)
|
||||||
|
<< SendData(server, b"handshake-hello")
|
||||||
|
)
|
||||||
|
if disconnect == "client":
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> ConnectionClosed(tctx.client)
|
||||||
|
>> ConnectionClosed(server) # proxyserver will cancel all other connections as well.
|
||||||
|
<< CloseConnection(server)
|
||||||
|
<< Log("Opened: err='connection closed without notice'. Server state: CLOSED")
|
||||||
|
<< Log("Got client close.")
|
||||||
|
<< CloseConnection(tctx.client)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert (
|
||||||
|
playbook
|
||||||
|
>> ConnectionClosed(server)
|
||||||
|
<< CloseConnection(server)
|
||||||
|
<< Log("Opened: err='connection closed without notice'. Server state: CLOSED")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_layer_stack(tctx):
|
||||||
|
stack = tunnel.LayerStack()
|
||||||
|
a = TChildLayer(tctx)
|
||||||
|
b = TChildLayer(tctx)
|
||||||
|
stack /= a
|
||||||
|
stack /= b
|
||||||
|
assert stack[0] == a
|
||||||
|
assert a.child_layer is b
|
Loading…
Reference in New Issue
Block a user