11 Commits

Author SHA1 Message Date
Salad Dais
3149d3610f Pin cx_freeze version 2025-04-18 04:30:11 +00:00
Salad Dais
f8f3bcfc36 Make PyPi stop whining about attestations 2025-04-18 04:26:42 +00:00
Salad Dais
8548cce4e5 Use new upload-artifact action 2025-04-18 04:19:52 +00:00
Salad Dais
ad2aca1803 Upgrade mitmproxy 2025-04-18 01:44:23 +00:00
Salad Dais
8cf500ce44 Me more verbose if we can't parse legacy schema 2025-04-18 01:43:10 +00:00
Salad Dais
ceda7f370e Update message template to upstream 2024-12-11 22:59:27 +00:00
Salad Dais
0692a10253 Add support for JankStringyBytes in LLSD 2024-12-11 22:58:56 +00:00
Salad Dais
c1c2a96295 Fix some event handling quirks 2024-12-11 22:56:50 +00:00
Salad Dais
b4be9fa757 Better handle resent reliable messages 2024-10-29 07:31:59 +00:00
Salad Dais
a8967f0b7d Handle unknown messages better 2024-10-29 07:31:35 +00:00
Salad Dais
10af5cc250 Handle more JankStringyBytes ops 2024-10-29 07:15:24 +00:00
22 changed files with 6432 additions and 6210 deletions

View File

@@ -38,7 +38,8 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .
pip install cx_freeze
# Pinned for now until I figure out why it freaks out on bundling PySide6
pip install cx_freeze==6.15.15
- name: Bundle with cx_Freeze
shell: bash
@@ -51,7 +52,7 @@ jobs:
mv ./dist/*.zip hippolyzer-windows-${{ env.target_tag }}.zip
- name: Upload the artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: hippolyzer-windows-${{ env.sha }}
path: ./hippolyzer-windows-${{ env.target_tag }}.zip

View File

@@ -36,6 +36,7 @@ jobs:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
attestations: false
- name: Publish to PyPI
if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
@@ -43,3 +44,4 @@ jobs:
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
attestations: false

View File

@@ -322,12 +322,12 @@ class JankStringyBytes(bytes):
def __add__(self, other):
if isinstance(other, bytes):
return bytes(self) + other
return JankStringyBytes(bytes(self) + other)
return str(self) + other
def __radd__(self, other):
if isinstance(other, bytes):
return other + bytes(self)
return JankStringyBytes(other + bytes(self))
return other + str(self)
def lower(self):
@@ -336,6 +336,20 @@ class JankStringyBytes(bytes):
def upper(self):
return str(self).upper()
def startswith(self, __prefix, __start=None, __end=None):
if __start or __end:
raise RuntimeError("Can't handle __start or __end")
if isinstance(__prefix, str):
return str(self).startswith(__prefix)
return self.startswith(__prefix)
def endswith(self, __prefix, __start=None, __end=None):
if __start or __end:
raise RuntimeError("Can't handle __start or __end")
if isinstance(__prefix, str):
return str(self).endswith(__prefix)
return self.endswith(__prefix)
class RawBytes(bytes):
__slots__ = ()

View File

@@ -41,7 +41,8 @@ class Event:
return self
def _handler_key(self, handler):
@staticmethod
def _handler_key(handler):
return handler[:3]
def unsubscribe(self, handler, *args, **kwargs):
@@ -55,21 +56,23 @@ class Event:
raise ValueError(f"Handler {handler!r} is not subscribed to this event.")
return self
def _create_async_wrapper(self, handler, args, inner_args, kwargs):
# Note that unsubscription may be delayed due to asyncio scheduling :)
async def _run_handler_wrapper():
unsubscribe = await handler(args, *inner_args, **kwargs)
if unsubscribe:
_ = self.unsubscribe(handler, *inner_args, **kwargs)
return _run_handler_wrapper
def notify(self, args):
for handler in self.subscribers[:]:
handler, inner_args, kwargs, one_shot, predicate = handler
for subscriber in self.subscribers[:]:
handler, inner_args, kwargs, one_shot, predicate = subscriber
if predicate and not predicate(args):
continue
if one_shot:
self.unsubscribe(handler, *inner_args, **kwargs)
if asyncio.iscoroutinefunction(handler):
# Note that unsubscription may be delayed due to asyncio scheduling :)
async def _run_handler_wrapper():
unsubscribe = await handler(args, *inner_args, **kwargs)
if unsubscribe:
_ = self.unsubscribe(handler, *inner_args, **kwargs)
create_logged_task(_run_handler_wrapper(), self.name, LOG)
create_logged_task(self._create_async_wrapper(handler, args, inner_args, kwargs)(), self.name, LOG)
else:
try:
if handler(args, *inner_args, **kwargs) and not one_shot:

View File

@@ -171,7 +171,7 @@ def get_mtime(path):
def fut_logger(name: str, logger: logging.Logger, fut: asyncio.Future, *args) -> None:
"""Callback suitable for exception logging in `Future.add_done_callback()`"""
if fut.exception():
if not fut.cancelled() and fut.exception():
if isinstance(fut.exception(), asyncio.CancelledError):
# Don't really care if the task was just cancelled
return

View File

@@ -190,32 +190,36 @@ class SchemaBase(abc.ABC):
def from_llsd(cls, inv_dict: Dict, flavor: str = "legacy"):
fields = cls._get_fields_dict(llsd_flavor=flavor)
obj_dict = {}
for key, val in inv_dict.items():
if key in fields:
field: dataclasses.Field = fields[key]
key = field.name
spec = field.metadata.get("spec")
# Not a real key, an internal var on our dataclass
if not spec:
LOG.warning(f"Internal key {key!r}")
continue
try:
for key, val in inv_dict.items():
if key in fields:
field: dataclasses.Field = fields[key]
key = field.name
spec = field.metadata.get("spec")
# Not a real key, an internal var on our dataclass
if not spec:
LOG.warning(f"Internal key {key!r}")
continue
spec_cls = spec
if not inspect.isclass(spec_cls):
spec_cls = spec_cls.__class__
spec_cls = spec
if not inspect.isclass(spec_cls):
spec_cls = spec_cls.__class__
# some kind of nested structure like sale_info
if issubclass(spec_cls, SchemaBase):
obj_dict[key] = spec.from_llsd(val, flavor)
elif issubclass(spec_cls, SchemaFieldSerializer):
obj_dict[key] = spec.from_llsd(val, flavor)
# some kind of nested structure like sale_info
if issubclass(spec_cls, SchemaBase):
obj_dict[key] = spec.from_llsd(val, flavor)
elif issubclass(spec_cls, SchemaFieldSerializer):
obj_dict[key] = spec.from_llsd(val, flavor)
else:
raise ValueError(f"Unsupported spec for {key!r}, {spec!r}")
else:
raise ValueError(f"Unsupported spec for {key!r}, {spec!r}")
else:
if flavor != "ais":
# AIS has a number of different fields that are irrelevant depending on
# what exactly sent the payload
LOG.warning(f"Unknown key {key!r}")
if flavor != "ais":
# AIS has a number of different fields that are irrelevant depending on
# what exactly sent the payload
LOG.warning(f"Unknown key {key!r}")
except:
LOG.error(f"Failed to parse inventory schema: {inv_dict!r}")
raise
return cls._obj_from_dict(obj_dict)
def to_bytes(self) -> bytes:

View File

@@ -16,10 +16,12 @@ from hippolyzer.lib.base.datatypes import *
class HippoLLSDBaseFormatter(base_llsd.base.LLSDBaseFormatter):
UUID: callable
ARRAY: callable
BINARY: callable
def __init__(self):
super().__init__()
self.type_map[UUID] = self.UUID
self.type_map[JankStringyBytes] = self.BINARY
self.type_map[Vector2] = self.TUPLECOORD
self.type_map[Vector3] = self.TUPLECOORD
self.type_map[Vector4] = self.TUPLECOORD
@@ -101,7 +103,7 @@ def _format_binary_recurse(something) -> bytes:
raise LLSDSerializationError(str(exc), something)
elif isinstance(something, uuid.UUID):
return b'u' + something.bytes
elif isinstance(something, binary):
elif isinstance(something, (binary, JankStringyBytes)):
return b'b' + struct.pack('!i', len(something)) + something
elif is_string(something):
if is_unicode(something):

File diff suppressed because it is too large Load Diff

View File

@@ -188,7 +188,7 @@ class MsgBlockList(List["Block"]):
class Message:
__slots__ = ("name", "send_flags", "packet_id", "acks", "body_boundaries", "queued",
"offset", "raw_extra", "raw_body", "deserializer", "_blocks", "finalized",
"direction", "meta", "synthetic", "dropped", "sender")
"direction", "meta", "synthetic", "dropped", "sender", "unknown_message")
def __init__(self, name, *args, packet_id=None, flags=0, acks=None, direction=None):
# TODO: Do this on a timer or something.
@@ -200,6 +200,7 @@ class Message:
self.acks = acks if acks is not None else tuple()
self.body_boundaries = (-1, -1)
self.unknown_message = False
self.offset = 0
self.raw_extra = b""
self.direction: Direction = direction if direction is not None else Direction.OUT
@@ -288,7 +289,7 @@ class Message:
def ensure_parsed(self):
# This is a little magic, think about whether we want this.
if self.raw_body and self.deserializer():
if self.raw_body and self.deserializer and self.deserializer():
self.deserializer().parse_message_body(self)
def to_dict(self, extended=False):

View File

@@ -126,8 +126,14 @@ class UDPMessageDeserializer:
frequency, num = _parse_msg_num(reader)
current_template = self.template_dict.get_template_by_pair(frequency, num)
if current_template is None:
raise exc.MessageTemplateNotFound("deserializing data", f"{frequency}:{num}")
msg.name = current_template.name
if self.settings.ALLOW_UNKNOWN_MESSAGES:
LOG.warning(f"Unknown message type {frequency}:{num}")
msg.unknown_message = True
msg.name = "UnknownMessage:%d" % num
else:
raise exc.MessageTemplateNotFound("deserializing data", f"{frequency}:{num}")
else:
msg.name = current_template.name
# extra field, see note regarding msg.offset
msg.raw_extra = reader.read_bytes(msg.offset)
@@ -143,6 +149,12 @@ class UDPMessageDeserializer:
# Already parsed if we don't have a raw body
if not raw_body:
return
if msg.unknown_message:
# We can't parse this, we don't know anything about it
msg.deserializer = None
return
msg.raw_body = None
msg.deserializer = None

View File

@@ -45,7 +45,7 @@ class UDPMessageSerializer:
def serialize(self, msg: Message):
current_template = self.template_dict.get_template_by_name(msg.name)
if current_template is None:
if current_template is None and msg.raw_body is None:
raise exc.MessageSerializationError("message name", "invalid message name")
# Header and trailers are all big-endian

View File

@@ -1728,7 +1728,6 @@ class QuantizedNumPyArray(Adapter):
def subfield_serializer(msg_name, block_name, var_name):
def f(orig_cls):
global SUBFIELD_SERIALIZERS
SUBFIELD_SERIALIZERS[(msg_name, block_name, var_name)] = orig_cls
return orig_cls
return f
@@ -1940,7 +1939,6 @@ class IntFlagSubfieldSerializer(AdapterInstanceSubfieldSerializer):
def http_serializer(msg_name):
def f(orig_cls):
global HTTP_SERIALIZERS
HTTP_SERIALIZERS[msg_name] = orig_cls
return orig_cls
return f

View File

@@ -55,6 +55,7 @@ class SettingDescriptor(Generic[_T]):
class Settings:
ENABLE_DEFERRED_PACKET_PARSING: bool = SettingDescriptor(True)
ALLOW_UNKNOWN_MESSAGES: bool = SettingDescriptor(True)
def __init__(self):
self._settings: Dict[str, Any] = {}

View File

@@ -1822,9 +1822,20 @@ class ChatSourceType(IntEnum):
UNKNOWN = 3
@dataclasses.dataclass
class ThrottleData:
resend: float = se.dataclass_field(se.F32)
land: float = se.dataclass_field(se.F32)
wind: float = se.dataclass_field(se.F32)
cloud: float = se.dataclass_field(se.F32)
task: float = se.dataclass_field(se.F32)
texture: float = se.dataclass_field(se.F32)
asset: float = se.dataclass_field(se.F32)
@se.subfield_serializer("AgentThrottle", "Throttle", "Throttles")
class AgentThrottlesSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = se.Collection(None, se.F32)
TEMPLATE = se.Dataclass(ThrottleData)
@se.subfield_serializer("ObjectUpdate", "ObjectData", "NameValue")

View File

@@ -5,6 +5,7 @@ Body parts and linden clothing layers
from __future__ import annotations
import dataclasses
import enum
import logging
from io import StringIO
from typing import *
@@ -21,6 +22,60 @@ LOG = logging.getLogger(__name__)
_T = TypeVar("_T")
WEARABLE_VERSION = "LLWearable version 22"
DEFAULT_WEARABLE_TEX = UUID("c228d1cf-4b5d-4ba8-84f4-899a0796aa97")
class AvatarTEIndex(enum.IntEnum):
"""From llavatarappearancedefines.h"""
HEAD_BODYPAINT = 0
UPPER_SHIRT = enum.auto()
LOWER_PANTS = enum.auto()
EYES_IRIS = enum.auto()
HAIR = enum.auto()
UPPER_BODYPAINT = enum.auto()
LOWER_BODYPAINT = enum.auto()
LOWER_SHOES = enum.auto()
HEAD_BAKED = enum.auto()
UPPER_BAKED = enum.auto()
LOWER_BAKED = enum.auto()
EYES_BAKED = enum.auto()
LOWER_SOCKS = enum.auto()
UPPER_JACKET = enum.auto()
LOWER_JACKET = enum.auto()
UPPER_GLOVES = enum.auto()
UPPER_UNDERSHIRT = enum.auto()
LOWER_UNDERPANTS = enum.auto()
SKIRT = enum.auto()
SKIRT_BAKED = enum.auto()
HAIR_BAKED = enum.auto()
LOWER_ALPHA = enum.auto()
UPPER_ALPHA = enum.auto()
HEAD_ALPHA = enum.auto()
EYES_ALPHA = enum.auto()
HAIR_ALPHA = enum.auto()
HEAD_TATTOO = enum.auto()
UPPER_TATTOO = enum.auto()
LOWER_TATTOO = enum.auto()
HEAD_UNIVERSAL_TATTOO = enum.auto()
UPPER_UNIVERSAL_TATTOO = enum.auto()
LOWER_UNIVERSAL_TATTOO = enum.auto()
SKIRT_TATTOO = enum.auto()
HAIR_TATTOO = enum.auto()
EYES_TATTOO = enum.auto()
LEFT_ARM_TATTOO = enum.auto()
LEFT_LEG_TATTOO = enum.auto()
AUX1_TATTOO = enum.auto()
AUX2_TATTOO = enum.auto()
AUX3_TATTOO = enum.auto()
LEFTARM_BAKED = enum.auto()
LEFTLEG_BAKED = enum.auto()
AUX1_BAKED = enum.auto()
AUX2_BAKED = enum.auto()
AUX3_BAKED = enum.auto()
@property
def is_baked(self) -> bool:
return self.name.endswith("_BAKED")
@dataclasses.dataclass

View File

@@ -23,7 +23,7 @@ from hippolyzer.lib.base.message.udpdeserializer import UDPMessageDeserializer
from hippolyzer.lib.base.network.caps_client import CapsClient, CAPS_DICT
from hippolyzer.lib.base.network.transport import ADDR_TUPLE, Direction, SocketUDPTransport, AbstractUDPTransport
from hippolyzer.lib.base.settings import Settings, SettingDescriptor
from hippolyzer.lib.base.templates import RegionHandshakeReplyFlags, ChatType
from hippolyzer.lib.base.templates import RegionHandshakeReplyFlags, ChatType, ThrottleData
from hippolyzer.lib.base.transfer_manager import TransferManager
from hippolyzer.lib.base.xfer_manager import XferManager
from hippolyzer.lib.client.asset_uploader import AssetUploader
@@ -108,8 +108,9 @@ class HippoClientProtocol(asyncio.DatagramProtocol):
if should_handle:
self.session.message_handler.handle(message)
except:
LOG.exception("Failed in region message handler")
region.message_handler.handle(message)
LOG.exception("Failed in session message handler")
if should_handle:
region.message_handler.handle(message)
class HippoClientRegion(BaseClientRegion):
@@ -189,7 +190,7 @@ class HippoClientRegion(BaseClientRegion):
"RegionInfo",
Flags=(
RegionHandshakeReplyFlags.SUPPORTS_SELF_APPEARANCE
| RegionHandshakeReplyFlags.VOCACHE_IS_EMPTY
| RegionHandshakeReplyFlags.VOCACHE_CULLING_ENABLED
)
)
)
@@ -207,7 +208,15 @@ class HippoClientRegion(BaseClientRegion):
"Throttle",
GenCounter=0,
# Reasonable defaults, I guess
Throttles_=[207360.0, 165376.0, 33075.19921875, 33075.19921875, 682700.75, 682700.75, 269312.0],
Throttles_=ThrottleData(
resend=207360.0,
land=165376.0,
wind=33075.19921875,
cloud=33075.19921875,
task=682700.75,
texture=682700.75,
asset=269312.0
),
)
)
)
@@ -276,21 +285,25 @@ class HippoClientRegion(BaseClientRegion):
ack: Optional[int] = None
while True:
payload = {"ack": ack, "done": False}
async with self.caps_client.post("EventQueueGet", llsd=payload) as resp:
if resp.status != 200:
await asyncio.sleep(0.1)
continue
polled = await resp.read_llsd()
for event in polled["events"]:
if self._llsd_serializer.can_handle(event["message"]):
msg = self._llsd_serializer.deserialize(event)
else:
msg = Message.from_eq_event(event)
msg.sender = self.circuit_addr
msg.direction = Direction.IN
self.session().message_handler.handle(msg)
self.message_handler.handle(msg)
ack = polled["id"]
try:
async with self.caps_client.post("EventQueueGet", llsd=payload) as resp:
if resp.status != 200:
await asyncio.sleep(0.1)
continue
polled = await resp.read_llsd()
for event in polled["events"]:
if self._llsd_serializer.can_handle(event["message"]):
msg = self._llsd_serializer.deserialize(event)
else:
msg = Message.from_eq_event(event)
msg.sender = self.circuit_addr
msg.direction = Direction.IN
self.session().message_handler.handle(msg)
self.message_handler.handle(msg)
ack = polled["id"]
await asyncio.sleep(0.001)
except aiohttp.client_exceptions.ServerDisconnectedError:
# This is expected to happen during long-polling, just pick up again where we left off.
await asyncio.sleep(0.001)
async def _handle_ping_check(self, message: Message):

View File

@@ -116,18 +116,6 @@ class IPCInterceptionAddon:
self.to_proxy_queue: multiprocessing.Queue = flow_context.to_proxy_queue
self.shutdown_signal: multiprocessing.Event = flow_context.shutdown_signal
def add_log(self, entry: mitmproxy.log.LogEntry):
if entry.level == "debug":
logging.debug(entry.msg)
elif entry.level in ("alert", "info"):
# TODO: All mitmproxy infos are basically debugs, should
# probably give these dedicated loggers
logging.debug(entry.msg)
elif entry.level == "warn":
logging.warning(entry.msg)
elif entry.level == "error":
logging.error(entry.msg)
def running(self):
# register to pump the events or something here
create_logged_task(self._pump_callbacks(), "Pump HTTP proxy callbacks")

View File

@@ -100,9 +100,9 @@ setup(
# Proxy-specific stuff
'outleap<1.0',
'arpeggio',
# 11.x will be a major change.
'mitmproxy>=10.0.0,<11',
'Werkzeug<3.0',
# 12.x will be a major change.
'mitmproxy>=11.0.0,<12',
'Werkzeug<4.0',
# For REPLs
'ptpython<4.0',
# These could be in extras_require if you don't want a GUI.

View File

@@ -49,3 +49,15 @@ class TestEvents(unittest.IsolatedAsyncioTestCase):
await called.wait()
mock.assert_called_with("foo")
self.assertNotIn(_mock_wrapper, [x[0] for x in self.event.subscribers])
async def test_multiple_subscribers(self):
called = asyncio.Event()
called2 = asyncio.Event()
self.event.subscribe(lambda *args: called.set())
self.event.subscribe(lambda *args: called2.set())
self.event.notify(None)
self.assertTrue(called.is_set())
self.assertTrue(called2.is_set())

View File

@@ -181,6 +181,8 @@ class TestMessageHandlers(unittest.IsolatedAsyncioTestCase):
self.message_handler.handle(msg)
async def test_subscription(self):
called = asyncio.Event()
called2 = asyncio.Event()
with self.message_handler.subscribe_async(
message_names=("Foo",),
predicate=lambda m: m["Bar"]["Baz"] == 1,
@@ -192,6 +194,10 @@ class TestMessageHandlers(unittest.IsolatedAsyncioTestCase):
msg3 = Message("Foo", Block("Bar", Baz=1, Biz=3))
self._fake_received_message(msg1)
self._fake_received_message(msg2)
self.message_handler.subscribe("Foo", lambda *args: called.set())
self.message_handler.subscribe("Foo", lambda *args: called2.set())
self._fake_received_message(msg3)
received = []
while True:
@@ -199,14 +205,15 @@ class TestMessageHandlers(unittest.IsolatedAsyncioTestCase):
received.append(await asyncio.wait_for(get_msg(), 0.001))
except asyncio.exceptions.TimeoutError:
break
self.assertEqual(len(foo_handlers), 1)
self.assertEqual(len(foo_handlers), 3)
self.assertListEqual(received, [msg1, msg3])
# The message should have been take()n, making a copy
self.assertIsNot(msg1, received[0])
# take() was called, so this should have been marked queued
self.assertTrue(msg1.queued)
# Leaving the block should have unsubscribed automatically
self.assertEqual(len(foo_handlers), 0)
self.assertEqual(len(foo_handlers), 2)
self.assertTrue(called.is_set())
async def test_subscription_no_take(self):
with self.message_handler.subscribe_async(("Foo",), take=False) as get_msg:

View File

@@ -50,6 +50,8 @@ OBJECT_UPDATE = binascii.unhexlify(''.join(OBJECT_UPDATE.split()))
COARSE_LOCATION_UPDATE = b'\x00\x00\x00\x00E\x00\xff\x06\x00\xff\xff\xff\xff\x00'
UNKNOWN_PACKET = b'\x00\x00\x00\x00E\x00\xff\xf0\x00\xff\xff\xff\xff\x00'
class TestPacketDecode(unittest.TestCase):
@@ -110,3 +112,12 @@ class TestPacketDecode(unittest.TestCase):
parsed = deserializer.deserialize(message)
logging.debug("Parsed blocks: %r " % (list(parsed.blocks.keys()),))
self.assertEqual(message, serializer.serialize(parsed))
def test_unknown_packet_roundtrips(self):
message = UNKNOWN_PACKET
deserializer = UDPMessageDeserializer(settings=self.settings)
serializer = UDPMessageSerializer()
parsed = deserializer.deserialize(message)
logging.debug("Parsed blocks: %r " % (list(parsed.blocks.keys()),))
self.assertEqual("UnknownMessage:240", parsed.name)
self.assertEqual(message, serializer.serialize(parsed))

View File

@@ -21,6 +21,9 @@ from hippolyzer.lib.proxy.sessions import Session
from hippolyzer.lib.proxy.test_utils import BaseProxyTest
UNKNOWN_PACKET = b'\x00\x00\x00\x00E\x00\xff\xf0\x00\xff\xff\xff\xff\x00'
class MockAddon(BaseAddon):
def __init__(self):
self.events = []
@@ -242,6 +245,21 @@ class LLUDPIntegrationTests(BaseProxyTest):
self.assertEqual(entry.name, "UndoLand")
self.assertEqual(entry.message.dropped, True)
async def test_logging_unknown_message(self):
message_logger = SimpleMessageLogger()
self.session_manager.message_logger = message_logger
self._setup_default_circuit()
self.protocol.datagram_received(UNKNOWN_PACKET, self.region_addr)
await self._wait_drained()
entries = message_logger.entries
self.assertEqual(len(entries), 1)
entry: LLUDPMessageLogEntry = entries[0] # type: ignore
# Freezing shouldn't affect this
entry.freeze()
self.assertEqual(entry.name, "UnknownMessage:240")
self.assertEqual(entry.message.dropped, False)
self.assertEqual(entry.message.unknown_message, True)
async def test_session_message_handler(self):
self._setup_default_circuit()
obj_update = self._make_objectupdate_compressed(1234)