From 49a9c6f28fe8a4d695730509465ed6a2c21f3e7d Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Wed, 23 Jun 2021 07:03:06 +0000 Subject: [PATCH] Workaround for failed teleports due to EventQueue timeouts Closes #16 --- hippolyzer/apps/proxy_gui.py | 4 +-- hippolyzer/lib/proxy/http_event_manager.py | 30 +++++++++++++++++++--- hippolyzer/lib/proxy/region.py | 21 +++++++++++++-- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/hippolyzer/apps/proxy_gui.py b/hippolyzer/apps/proxy_gui.py index 1301e5a..0ba2622 100644 --- a/hippolyzer/apps/proxy_gui.py +++ b/hippolyzer/apps/proxy_gui.py @@ -632,7 +632,7 @@ class MessageBuilderWindow(QtWidgets.QMainWindow): msg = HumanMessageSerializer.from_human_string(msg_text, replacements, env, safe=False) if self.checkLLUDPViaCaps.isChecked(): if msg.direction == Direction.IN: - region.eq_manager.queue_event( + region.eq_manager.inject_event( self.llsdSerializer.serialize(msg, as_dict=True) ) else: @@ -656,7 +656,7 @@ class MessageBuilderWindow(QtWidgets.QMainWindow): raise RuntimeError("Need a valid session and region to send EQ event") message_line, _, body = (x.strip() for x in msg_text.partition("\n")) message_name = message_line.rsplit(" ", 1)[-1] - region.eq_manager.queue_event({ + region.eq_manager.inject_event({ "message": message_name, "body": llsd.parse_xml(body.encode("utf8")), }) diff --git a/hippolyzer/lib/proxy/http_event_manager.py b/hippolyzer/lib/proxy/http_event_manager.py index bf588e9..409ae87 100644 --- a/hippolyzer/lib/proxy/http_event_manager.py +++ b/hippolyzer/lib/proxy/http_event_manager.py @@ -137,6 +137,27 @@ class MITMProxyEventManager: # the proxy self._asset_server_proxied = True logging.warning("noproxy not used, switching to URI rewrite strategy") + elif cap_data and cap_data.cap_name == "EventQueueGet": + # HACK: The sim's EQ acking mechanism doesn't seem to actually work. + # if the client drops the connection due to timeout before we can + # proxy back the response then it will be lost forever. Keep around + # the last EQ response we got so we can re-send it if the client repeats + # its previous request. + req_ack_id = llsd.parse_xml(flow.request.content)["ack"] + eq_manager = cap_data.region().eq_manager + cached_resp = eq_manager.get_cached_poll_response(req_ack_id) + if cached_resp: + logging.warning("Had to serve a cached EventQueueGet due to client desync") + flow.response = mitmproxy.http.HTTPResponse.make( + 200, + llsd.format_xml(cached_resp), + { + "Content-Type": "application/llsd+xml", + # So we can differentiate these in the log + "X-Hippo-Fake-EQ": "1", + "Connection": "close", + }, + ) elif not cap_data: if self._is_login_request(flow): # Not strictly a Cap, but makes it easier to filter on. @@ -251,11 +272,14 @@ class MITMProxyEventManager: new_events.append(event) # Add on any fake events that've been queued by addons eq_manager = cap_data.region().eq_manager - new_events.extend(eq_manager.take_events()) + new_events.extend(eq_manager.take_injected_events()) parsed_eq_resp["events"] = new_events + # Empty event list is an error, need to return undef instead. if old_events and not new_events: - # Need at least one event or the viewer will refuse to ack! - new_events.append({"message": "NOP", "body": {}}) + parsed_eq_resp = None + # HACK: see note in above request handler for EventQueueGet + req_ack_id = llsd.parse_xml(flow.request.content)["ack"] + eq_manager.cache_last_poll_response(req_ack_id, parsed_eq_resp) flow.response.content = llsd.format_pretty_xml(parsed_eq_resp) elif cap_data.cap_name in self.UPLOAD_CREATING_CAPS: if not region: diff --git a/hippolyzer/lib/proxy/region.py b/hippolyzer/lib/proxy/region.py index 024ba33..57ee080 100644 --- a/hippolyzer/lib/proxy/region.py +++ b/hippolyzer/lib/proxy/region.py @@ -162,6 +162,7 @@ class ProxiedRegion(BaseClientRegion): if self.circuit: self.circuit.is_alive = False self.objects.clear() + self.eq_manager.clear() def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.name) @@ -172,11 +173,27 @@ class EventQueueManager: # TODO: Per-EQ InjectionTracker so we can inject fake responses on 499 self._queued_events = [] self._region = weakref.proxy(region) + self._last_ack: Optional[int] = None + self._last_payload: Optional[Any] = None - def queue_event(self, event: dict): + def inject_event(self, event: dict): self._queued_events.append(event) - def take_events(self): + def take_injected_events(self): events = self._queued_events self._queued_events = [] return events + + def cache_last_poll_response(self, req_ack: int, payload: Any): + self._last_ack = req_ack + self._last_payload = payload + + def get_cached_poll_response(self, req_ack: Optional[int]) -> Optional[Any]: + if self._last_ack == req_ack: + return self._last_payload + return None + + def clear(self): + self._queued_events.clear() + self._last_ack = None + self._last_payload = None