import asyncio import gc import logging import multiprocessing import os import sys import time from typing import Optional import mitmproxy.exceptions from hippolyzer.lib.base import llsd from hippolyzer.lib.proxy.addons import AddonManager from hippolyzer.lib.proxy.addon_utils import BaseAddon from hippolyzer.lib.proxy.ca_utils import setup_ca from hippolyzer.lib.proxy.commands import handle_command from hippolyzer.lib.proxy.http_proxy import create_http_proxy, create_proxy_master, HTTPFlowContext from hippolyzer.lib.proxy.http_event_manager import MITMProxyEventManager from hippolyzer.lib.proxy.lludp_proxy import SLSOCKS5Server from hippolyzer.lib.proxy.message import ProxiedMessage from hippolyzer.lib.proxy.region import ProxiedRegion from hippolyzer.lib.proxy.sessions import SessionManager, Session LOG = logging.getLogger(__name__) class SelectionManagerAddon(BaseAddon): def handle_lludp_message(self, session: Session, region: ProxiedRegion, message: ProxiedMessage): selected = session.selected if message.name == "ObjectSelect": # ObjectDeselect intentionally ignored to deal with messages that # won't work while the object is still selected selected.object_local = message["ObjectData"][-1]["ObjectLocalID"] needed_objects = set() selected.object_locals = tuple(b["ObjectLocalID"] for b in message["ObjectData"]) for local_id in selected.object_locals: obj = region.objects.lookup_localid(local_id) if obj: if not obj.ParentID: selected.object_local = obj.LocalID else: LOG.debug(f"Don't know about selected {local_id}, requesting object") needed_objects.add(local_id) if needed_objects: region.objects.request_objects(needed_objects) # ParcelDwellRequests are sent whenever "about land" is opened. This gives us a # decent mechanism for selecting parcels. elif message.name == "ParcelDwellReply": block = message["Data"] selected.parcel_full = block["ParcelID"] selected.parcel_local = block["LocalID"] elif message.name == "UpdateTaskInventory": # This isn't quite right because UpdateTaskInventory is basically an # upsert, and takes on the insert behaviour if ItemID isn't in the task # already. The insert behaviour will give it a _new_ ItemID. # We have no way to distinguish the two without additional context. # You can always tick the share to group box though, and that should trigger # an update with the correct, new item ID. selected.task_item = message["InventoryData"]["ItemID"] def handle_http_request(self, session_manager, flow): cap_data = flow.cap_data if not cap_data: return if not cap_data.session: return selected = cap_data.session().selected # GetMetadata gets called whenever you open a script in a task, so we # can use it to guess which script you last opened. if cap_data.cap_name == "GetMetadata": parsed = llsd.parse_xml(flow.request.content) selected.script_item = parsed["item-id"] selected.task_item = parsed["item-id"] class REPLAddon(BaseAddon): @handle_command() async def spawn_repl(self, session: Session, region: ProxiedRegion): """Spawn a REPL""" if not AddonManager.have_active_repl(): AddonManager.spawn_repl() def run_http_proxy_process(proxy_host, http_proxy_port, flow_context: HTTPFlowContext): mitm_loop = asyncio.new_event_loop() asyncio.set_event_loop(mitm_loop) mitmproxy_master = create_http_proxy(proxy_host, http_proxy_port, flow_context) mitmproxy_master.start_server() gc.freeze() mitm_loop.run_forever() def start_proxy(extra_addons: Optional[list] = None, extra_addon_paths: Optional[list] = None, session_manager=None, proxy_host=None): extra_addons = extra_addons or [] extra_addon_paths = extra_addon_paths or [] extra_addons.append(SelectionManagerAddon()) extra_addons.append(REPLAddon()) root_log = logging.getLogger() root_log.addHandler(logging.StreamHandler()) root_log.setLevel(logging.INFO) logging.basicConfig() loop = asyncio.get_event_loop() udp_proxy_port = int(os.environ.get("HIPPO_UDP_PORT", 9061)) http_proxy_port = int(os.environ.get("HIPPO_HTTP_PORT", 9062)) if proxy_host is None: proxy_host = os.environ.get("HIPPO_BIND_HOST", "127.0.0.1") session_manager = session_manager or SessionManager() flow_context = session_manager.flow_context # TODO: argparse if len(sys.argv) == 3: if sys.argv[1] == "--setup-ca": try: mitmproxy_master = create_http_proxy(proxy_host, http_proxy_port, flow_context) except mitmproxy.exceptions.ServerException: # Proxy already running, create the master so we don't try to bind to a port mitmproxy_master = create_proxy_master(proxy_host, http_proxy_port, flow_context) setup_ca(sys.argv[2], mitmproxy_master) return sys.exit(0) http_proc = multiprocessing.Process( target=run_http_proxy_process, args=(proxy_host, http_proxy_port, flow_context), daemon=True, ) http_proc.start() server = SLSOCKS5Server(session_manager) coro = asyncio.start_server(server.handle_connection, proxy_host, udp_proxy_port) async_server = loop.run_until_complete(coro) event_manager = MITMProxyEventManager(session_manager, flow_context) loop.create_task(event_manager.pump_proxy_events()) addon_paths = sys.argv[1:] addon_paths.extend(extra_addon_paths) AddonManager.init(addon_paths, session_manager, addon_objects=extra_addons) # Everything in memory at this point should stay gc.freeze() gc.set_threshold(5000, 50, 10) # Serve requests until Ctrl+C is pressed print(f"SOCKS and HTTP proxies running on {proxy_host}") try: loop.run_forever() except KeyboardInterrupt: pass print("Asking mitmproxy to shut down") # Shut down mitmproxy flow_context.shutdown_signal.set() # Close the server print("Closing SOCKS server") async_server.close() print("Shutting down addons") AddonManager.shutdown() print("Waiting for SOCKS server to close") loop.run_until_complete(async_server.wait_closed()) print("Closing event loop") if sys.platform == "win32": # Hack around some bug with qasync's event loop hanging on win32 p = multiprocessing.Process( target=_windows_timeout_killer, args=(os.getpid(),) ) p.daemon = True p.start() loop.close() def _windows_timeout_killer(pid: int): time.sleep(2.0) print(f"Killing hanging event loop") os.kill(pid, 9) def main(): multiprocessing.set_start_method("spawn") start_proxy() if __name__ == "__main__": multiprocessing.freeze_support() main()