Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .audit/oberstet_fix_1850.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- [ ] I did **not** use any AI-assistance tools to help create this pull request.
- [x] I **did** use AI-assistance tools to *help* create this pull request.
- [x] I have read, understood and followed the projects' [AI Policy](https://github.com/crossbario/autobahn-python/blob/main/AI_POLICY.md) when creating code, documentation etc. for this pull request.

Submitted by: @oberstet
Date: 2026-06-17
Related issue(s): #1850
Branch: oberstet:fix_1850
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Changelog
26.6.1
------

**WAMP RawSocket**

* Fix the Twisted ``WampRawSocketProtocol`` raising ``TransportLost`` out of ``dataReceived`` when the opening handshake fails before a WAMP session is attached (e.g. an invalid magic byte from a port scanner). ``abort()`` now tears down the transport whenever a transport is present - rather than only when a session is open - so a failed handshake closes the connection cleanly with a single warning instead of an "Unhandled Error" stack trace, and handshake processing stops instead of continuing past the abort. The asyncio backend already behaved correctly; cross-backend regression tests were added for both. Thanks to @karel-un for the report (#1850)

**WAMP Serialization**

* ``py-ubjson`` (unmaintained, sdist-only) is no longer an unconditional dependency. A base ``pip install autobahn`` — and the wheels-only / cross-arch case from #1849 (``pip download --only-binary :all: --platform ...``) — now resolves entirely from binary wheels (#1849)
Expand Down
44 changes: 44 additions & 0 deletions src/autobahn/asyncio/test/test_aio_rawsocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,47 @@ def fact_client():
proto.data_received(d)
assert client.onMessage.called
assert isinstance(client.onMessage.call_args[0][0], message.Abort)


@pytest.mark.skipif(
not os.environ.get("USE_ASYNCIO", False), reason="test runs on asyncio only"
)
def test_wamp_server_bad_magic_byte_aborts_cleanly():
"""
A WAMP server receiving an invalid magic byte in the opening handshake
closes the transport cleanly and starts no session (cross-backend parity
with the Twisted fix for #1850).
"""
transport = Mock(spec_set=("abort", "close", "write", "get_extra_info"))
server = Mock(spec=["onOpen", "onMessage"])

proto = WampRawSocketServerFactory(lambda: server)()
proto.connection_made(transport)

# any non-0x7f first octet is an invalid magic byte; this must not raise
proto.data_received(b"GET ")

transport.close.assert_called_once_with()
server.onOpen.assert_not_called()


@pytest.mark.skipif(
not os.environ.get("USE_ASYNCIO", False), reason="test runs on asyncio only"
)
def test_wamp_client_bad_magic_byte_aborts_cleanly():
"""
A WAMP client receiving an invalid magic byte in the opening handshake
closes the transport cleanly and starts no session (cross-backend parity
with the Twisted fix for #1850).
"""
transport = Mock(spec_set=("abort", "close", "write", "get_extra_info"))
client = Mock(spec=["onOpen", "onMessage"])

proto = WampRawSocketClientFactory(lambda: client)()
proto.connection_made(transport)

# any non-0x7f first octet is an invalid magic byte; this must not raise
proto.data_received(b"GET ")

transport.close.assert_called_once_with()
client.onOpen.assert_not_called()
11 changes: 10 additions & 1 deletion src/autobahn/twisted/rawsocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,12 @@ def abort(self):
"""
Implements :func:`autobahn.wamp.interfaces.ITransport.abort`
"""
if self.isOpen():
# Guard on the transport, not isOpen(): isOpen() requires an attached
# WAMP session, but abort() must also tear down the transport when
# called before the session is established - e.g. on a failed opening
# handshake (bad magic byte / no suitable serializer), see #1850.
# Only a genuinely missing transport is a lost transport.
if self.transport is not None:
if hasattr(self.transport, "abortConnection"):
# ProcessProtocol lacks abortConnection()
self.transport.abortConnection()
Expand Down Expand Up @@ -338,6 +343,7 @@ def dataReceived(self, data):
magic=_magic,
)
self.abort()
return
else:
self.log.debug(
"WampRawSocketServerProtocol: correct magic byte received"
Expand Down Expand Up @@ -367,6 +373,7 @@ def dataReceived(self, data):
serializers=self.factory._serializers.keys(),
)
self.abort()
return

# we request the client to send message of maximum length 2**reply_max_len_exp
#
Expand Down Expand Up @@ -460,6 +467,7 @@ def dataReceived(self, data):
magic=_LazyHexFormatter(self._handshake_bytes[0]),
)
self.abort()
return

# peer requests us to _send_ messages of maximum length 2**max_len_exp
#
Expand All @@ -479,6 +487,7 @@ def dataReceived(self, data):
serializers=self._serializer.RAWSOCKET_SERIALIZER_ID,
)
self.abort()
return

self._handshake_complete = True

Expand Down
44 changes: 44 additions & 0 deletions src/autobahn/twisted/test/test_tx_rawsocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,47 @@ def test_handshake_succeeds(self):
# onOpen is called on the session
session_mock.onOpen.assert_called_once_with(p)
server_session_mock.onOpen.assert_called_once_with(sp)

def test_server_bad_magic_byte_aborts_cleanly(self):
"""
A server receiving an invalid magic byte in the opening handshake
aborts the transport cleanly instead of raising ``TransportLost`` out
of ``dataReceived`` (regression test for #1850).
"""
server_session_mock = Mock()
st = FakeTransport()
sf = WampRawSocketServerFactory(lambda: server_session_mock)
sp = WampRawSocketServerProtocol()
sp.transport = st
sp.factory = sf

sp.connectionMade()

# any non-0x7f first octet is an invalid magic byte; this must not raise
sp.dataReceived(b"GET ")

# the transport was aborted and no WAMP session was started
self.assertTrue(st.abort_called())
server_session_mock.onOpen.assert_not_called()

def test_client_bad_magic_byte_aborts_cleanly(self):
"""
A client receiving an invalid magic byte in the opening handshake
aborts the transport cleanly instead of raising ``TransportLost`` out
of ``dataReceived`` (regression test for #1850).
"""
session_mock = Mock()
t = FakeTransport()
f = WampRawSocketClientFactory(lambda: session_mock)
p = WampRawSocketClientProtocol()
p.transport = t
p.factory = f

p.connectionMade()

# any non-0x7f first octet is an invalid magic byte; this must not raise
p.dataReceived(b"GET ")

# the transport was aborted and no WAMP session was started
self.assertTrue(t.abort_called())
session_mock.onOpen.assert_not_called()
Loading