diff --git a/addon_examples/leap_example.py b/addon_examples/leap_example.py new file mode 100644 index 0000000..b95ed12 --- /dev/null +++ b/addon_examples/leap_example.py @@ -0,0 +1,43 @@ +""" +Example of how to control a viewer over LEAP + +Must launch the viewer with `outleap-agent` LEAP script. +See https://github.com/SaladDais/outleap/ for more info on LEAP / outleap. +""" + +import outleap +from outleap.scripts.inspector import LEAPInspectorGUI + +from hippolyzer.lib.proxy.addon_utils import send_chat, BaseAddon, show_message +from hippolyzer.lib.proxy.commands import handle_command +from hippolyzer.lib.proxy.region import ProxiedRegion +from hippolyzer.lib.proxy.sessions import Session + + +# Path found using `outleap-inspector` +FPS_PATH = outleap.UIPath("/main_view/menu_stack/status_bar_container/status/time_and_media_bg/FPSText") + + +class LEAPExampleAddon(BaseAddon): + @handle_command() + async def show_ui_inspector(self, session: Session, _region: ProxiedRegion): + """Spawn a GUI for inspecting the UI state""" + if not session.leap_client: + show_message("No LEAP client connected?") + return + LEAPInspectorGUI(session.leap_client).show() + + @handle_command() + async def say_fps(self, session: Session, _region: ProxiedRegion): + """Say your current FPS in chat""" + if not session.leap_client: + show_message("No LEAP client connected?") + return + + window_api = outleap.LLWindowAPI(session.leap_client) + fps = (await window_api.get_info(path=FPS_PATH))['value'] + + send_chat(f"LEAP says I'm running at {fps} FPS!") + + +addons = [LEAPExampleAddon()] diff --git a/hippolyzer/apps/proxy.py b/hippolyzer/apps/proxy.py index 3a1872f..847323a 100644 --- a/hippolyzer/apps/proxy.py +++ b/hippolyzer/apps/proxy.py @@ -9,6 +9,7 @@ from typing import Optional import mitmproxy.ctx import mitmproxy.exceptions +import outleap from hippolyzer.lib.base import llsd from hippolyzer.lib.proxy.addons import AddonManager @@ -112,6 +113,7 @@ def start_proxy(session_manager: SessionManager, extra_addons: Optional[list] = udp_proxy_port = session_manager.settings.SOCKS_PROXY_PORT http_proxy_port = session_manager.settings.HTTP_PROXY_PORT + leap_port = session_manager.settings.LEAP_PORT if proxy_host is None: proxy_host = session_manager.settings.PROXY_BIND_ADDR @@ -143,6 +145,10 @@ def start_proxy(session_manager: SessionManager, extra_addons: Optional[list] = coro = asyncio.start_server(server.handle_connection, proxy_host, udp_proxy_port) async_server = loop.run_until_complete(coro) + leap_server = outleap.LEAPBridgeServer(session_manager.leap_client_connected) + coro = asyncio.start_server(leap_server.handle_connection, proxy_host, leap_port) + async_leap_server = loop.run_until_complete(coro) + event_manager = MITMProxyEventManager(session_manager, flow_context) loop.create_task(event_manager.run()) @@ -169,6 +175,8 @@ def start_proxy(session_manager: SessionManager, extra_addons: Optional[list] = # Close the server print("Closing SOCKS server") async_server.close() + print("Shutting down LEAP server") + async_leap_server.close() print("Shutting down addons") AddonManager.shutdown() print("Waiting for SOCKS server to close") diff --git a/hippolyzer/lib/base/message/udpdeserializer.py b/hippolyzer/lib/base/message/udpdeserializer.py index 47e995d..c4c109b 100644 --- a/hippolyzer/lib/base/message/udpdeserializer.py +++ b/hippolyzer/lib/base/message/udpdeserializer.py @@ -157,7 +157,6 @@ class UDPMessageDeserializer: reader.seek(current_template.get_msg_freq_num_len() + msg.offset) for tmpl_block in current_template.blocks: - LOG.debug("Parsing %s:%s" % (msg.name, tmpl_block.name)) # EOF? if not len(reader): # Seems like even some "Single" blocks are optional? @@ -180,7 +179,6 @@ class UDPMessageDeserializer: for i in range(repeat_count): current_block = Block(tmpl_block.name) - LOG.debug("Adding block %s" % current_block.name) msg.add_block(current_block) for tmpl_variable in tmpl_block.variables: diff --git a/hippolyzer/lib/proxy/sessions.py b/hippolyzer/lib/proxy/sessions.py index 168f105..9b49379 100644 --- a/hippolyzer/lib/proxy/sessions.py +++ b/hippolyzer/lib/proxy/sessions.py @@ -9,6 +9,8 @@ import weakref from typing import * from weakref import ref +from outleap import LEAPClient + from hippolyzer.lib.base.datatypes import UUID from hippolyzer.lib.base.helpers import proxify from hippolyzer.lib.base.message.message import Message @@ -50,6 +52,7 @@ class Session(BaseClientSession): self.http_message_handler: MessageHandler[HippoHTTPFlow, str] = MessageHandler() self.objects = ProxyWorldObjectManager(self, session_manager.settings, session_manager.name_cache) self.inventory = ProxyInventoryManager(proxify(self)) + self.leap_client: Optional[LEAPClient] = None # Base path of a newview type cache directory for this session self.cache_dir: Optional[str] = None self._main_region = None @@ -187,6 +190,7 @@ class SessionManager: self.message_logger: Optional[BaseMessageLogger] = None self.addon_ctx: Dict[str, Any] = {} self.name_cache = ProxyNameCache() + self.pending_leap_clients: List[LEAPClient] = [] def create_session(self, login_data) -> Session: session = Session.from_login_data(login_data, self) @@ -203,12 +207,23 @@ class SessionManager: if session.pending and session.id == session_id: logging.info("Claimed %r" % session) session.pending = False + # TODO: less crap way of tying a LEAP client to a session + while self.pending_leap_clients: + leap_client = self.pending_leap_clients.pop(-1) + # Client may have gone bad since it connected + if not leap_client.connected: + continue + logging.info("Assigned LEAP client to session") + session.leap_client = leap_client + break return session return None def close_session(self, session: Session): logging.info("Closed %r" % session) session.objects.clear() + if session.leap_client: + session.leap_client.disconnect() self.sessions.remove(session) def resolve_cap(self, url: str) -> Optional["CapData"]: @@ -218,6 +233,9 @@ class SessionManager: return cap_data return CapData() + async def leap_client_connected(self, leap_client: LEAPClient): + self.pending_leap_clients.append(leap_client) + @dataclasses.dataclass class SelectionModel: diff --git a/hippolyzer/lib/proxy/settings.py b/hippolyzer/lib/proxy/settings.py index a722d32..66293b3 100644 --- a/hippolyzer/lib/proxy/settings.py +++ b/hippolyzer/lib/proxy/settings.py @@ -25,6 +25,7 @@ class EnvSettingDescriptor(SettingDescriptor): class ProxySettings(Settings): SOCKS_PROXY_PORT: int = EnvSettingDescriptor(9061, "HIPPO_UDP_PORT", int) HTTP_PROXY_PORT: int = EnvSettingDescriptor(9062, "HIPPO_HTTP_PORT", int) + LEAP_PORT: int = EnvSettingDescriptor(9063, "HIPPO_LEAP_PORT", int) PROXY_BIND_ADDR: str = EnvSettingDescriptor("127.0.0.1", "HIPPO_BIND_HOST", str) REMOTELY_ACCESSIBLE: bool = SettingDescriptor(False) USE_VIEWER_OBJECT_CACHE: bool = SettingDescriptor(False) diff --git a/requirements.txt b/requirements.txt index 3cee434..7d478fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,6 +35,7 @@ mitmproxy==8.0.0 msgpack==1.0.3 multidict==5.2.0 numpy==1.21.4 +outleap~=0.4.1 parso==0.8.3 passlib==1.7.4 prompt-toolkit==3.0.23 @@ -66,4 +67,4 @@ wcwidth==0.2.5 Werkzeug==2.0.2 wsproto==1.0.0 yarl==1.7.2 -zstandard==0.15.2 +zstandard==0.15.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 0a0976a..0e69361 100644 --- a/setup.py +++ b/setup.py @@ -82,6 +82,7 @@ setup( python_requires='>=3.8', install_requires=[ 'llsd<1.1.0', + 'outleap<1.0', 'defusedxml', 'aiohttp<4.0.0', 'recordclass<0.15',