From 7dd45fff2c26f7c3cabb99906eca12c54a6cbaad Mon Sep 17 00:00:00 2001 From: William Bruno Date: Thu, 12 Mar 2026 11:46:19 -0400 Subject: [PATCH] rewrite --- __pycache__/main.cpython-313.pyc | Bin 0 -> 54429 bytes __pycache__/main.cpython-314.pyc | Bin 0 -> 61845 bytes __pycache__/server.cpython-313.pyc | Bin 0 -> 35439 bytes __pycache__/server.cpython-314.pyc | Bin 0 -> 40647 bytes __pycache__/socktest.cpython-314.pyc | Bin 0 -> 3011 bytes main.py | 1008 +++----------------------- pyproject.toml | 5 +- server.py | 568 +++++++++++++++ socktest.py | 75 ++ uv.lock | 182 ++--- 10 files changed, 794 insertions(+), 1044 deletions(-) create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 __pycache__/main.cpython-314.pyc create mode 100644 __pycache__/server.cpython-313.pyc create mode 100644 __pycache__/server.cpython-314.pyc create mode 100644 __pycache__/socktest.cpython-314.pyc create mode 100644 server.py create mode 100644 socktest.py diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e21938306dd2ded3f9982121d7791649a30f7b37 GIT binary patch literal 54429 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 INF3t)Kd-mpHvj+t literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-314.pyc b/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6270b9fbf7a6624d485478f855573294494580d8 GIT binary patch literal 61845 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℘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 E7nk6DQ~&?~ literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-313.pyc b/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2a6cc3479cf1f94c8215e6829a0f0eef7577c71 GIT binary patch literal 35439 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 F{|g!g_%r|j literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-314.pyc b/__pycache__/server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27fc1aee21e70eb9038160adec0db005d7f98907 GIT binary patch literal 40647 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 Bf(8Hp literal 0 HcmV?d00001 diff --git a/__pycache__/socktest.cpython-314.pyc b/__pycache__/socktest.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..441fda9caf49c0b1d4d97ea2c36ff0e6d61b711c GIT binary patch literal 3011 zcmbVO&2QX96!&<&yQw!H{f;Otaon`K+Y&F3Mv!E)G^C{s3Tfm-NJxapu_xJ~UVAk& z8=4@blpcT+9CP51BbBIB;2+>0K#JOkNQR!^!Y!pIZoKiw?j~&!VkFzoesA8#?|pbK zc6X@~ti$)eCwDs}>38vxtaMB~(U2r0WZ6|VG23v$17 zDc-`<3({<7qNw%kmrTV>o0-?;=o`<1+~F2Kq57qT>_YldB6VmXJ9|9gwVr0Sw`DQ+ z9GQ+bna=0P9BPy4GF9Hake=;p*-0ofcL|UX%j|61ao9v>rDE5gf-zG>LH2En@rl_f zAUm(mzdLPWR`)%I1I1!1BtRF6a-dpl&9fJ4zJr6~woloj<+}dI*je-`Ss@;?=~5qLXcrS6W{uz51M;tlN`dnWSPj6%~S~`=w5@Ie3X); zTWBLSomN4+shpLv(sX7xMNl!lest0$*yEb*dmgqqc1E?JgEoV5EN-nIpG=q<_q91p zS269Mxtj{obxJ%Qq!{r71=k3N;Nisyve;WC6mSvFo5y4Hax#nv+9G4 z)COJOwm9*<(voH09K@beV!nNoW5$j8T9A!hqQ^kTQMgzMZhrzie<+8xJ73yln|NCt z*ip{?rgnW*__FZz=vHRCP}%N#^PzfqSGl~ST>hJhI{oazF!~M+DfhC&Xdm<$V204I z1MzI=P4pgu1StPG0RX?LgnQ(DX-FSPL%J;une}cH+nB6kM_Z=?3}RS9YpxI*YI0P$?- z+N)M{QCM;E`iZ#W9Kza)4tk4rfhu^2=$No|K8j0iNm3Ek8NnAZHfy1O>+IHvZ8iUk zQuvz*&ptbH3|T(|rI!1z+?bqyfA*?YHfh%ZQ8e-Ho-WsFoTmF1=_P^_^XoYQMtHddOf+M) zDYiI{!!778(ZP;DqkXw#U+C}5-Iy^Lhe^0Xme(SSPj70PH~^;}H!ahPhE=b_5S*+M z*D1K&Bd z*%hFY4a2RuQb2_>xB$e#8QeN2B)_hB0;3uDgNPv+Kq@D_UeuJlCcjV5lIcn-cr z#h{∋0OG4%=MsH@Szofo)|Vp0kIKqT-RDBPtR&1!R}ucx2TH6}|;W;x{8{NfsGS zL8S+|32{n_P7}0Sh-W9}u1!q3K=@+|e*yHAkWWD`g-+5J;gy~i zxA0HcQHHZu1BN-x!#D9OVfWy+mqG~rD)l^)UVJ27c$|^4-FL`FY7dB>-OQ;6nNwTK zKMn0OQ&Kb>>0WnN53Z=wVlWL*46;im!QIM{sGq4ymSBn literal 0 HcmV?d00001 diff --git a/main.py b/main.py index 4ad58d3..a8684dc 100644 --- a/main.py +++ b/main.py @@ -1,930 +1,96 @@ import asyncio +import dataclasses import json import logging -import socket -import time -from dataclasses import dataclass -from typing import Dict, Optional - -import uvicorn -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from pymobiledevice3.lockdown import create_using_usbmux -from pymobiledevice3.remote.common import TunnelProtocol -from pymobiledevice3.remote.remotexpc import RemoteXPCConnection -from pymobiledevice3.remote.tunnel_service import CoreDeviceTunnelProxy, start_tunnel, TunnelResult -from pymobiledevice3.service_connection import ServiceConnection -from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService -from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation -from pymobiledevice3.services.mobile_image_mounter import MobileImageMounterService -from pymobiledevice3.usbmux import list_devices -from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService - - -class JsonFormatter(logging.Formatter): - def format(self, record: logging.LogRecord) -> str: - payload = { - "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"), - "level": record.levelname, - "logger": record.name, - "message": record.getMessage(), - } - if record.exc_info: - payload["exc_info"] = self.formatException(record.exc_info) - return json.dumps(payload, ensure_ascii=True) - - -class LocationUpdate(BaseModel): - latitude: float - longitude: float - rsd_address: Optional[str] = None - rsd_port: Optional[int] = None - - -class DeviceShort(BaseModel): - udid: str - connection_type: str - - -class DeviceStatus(BaseModel): - device_connected: bool = False - device_count: int = 0 - devices: Optional[list[DeviceShort]] = None - udid: Optional[str] = None - device_name: Optional[str] = None - product_version: Optional[str] = None - phone_number: Optional[str] = None - developer_mode_enabled: Optional[bool] = None - ddi_mounted: Optional[bool] = None - rsd_address: Optional[str] = None - rsd_port: Optional[int] = None - lockdown_trusted_port: Optional[int] = None - lockdown_untrusted_port: Optional[int] = None - lockdown_trusted_reachable: bool = False - lockdown_untrusted_reachable: bool = False - dtservicehub_reachable: bool = False - - -class TunnelStartRequest(BaseModel): - protocol: str = "tcp" - wait_for_device: bool = False - wait_timeout_seconds: int = 30 - - -class PreflightResponse(BaseModel): - rsd_address: str - rsd_port: int - interface: Optional[str] = None - protocol: Optional[str] = None - dtservicehub_port: Optional[int] = None - dtservicehub_reachable: bool = False - lockdown_trusted_port: Optional[int] = None - lockdown_untrusted_port: Optional[int] = None - lockdown_trusted_reachable: bool = False - lockdown_untrusted_reachable: bool = False - - -@dataclass -class TunnelState: - task: Optional[asyncio.Task] - stop_event: asyncio.Event - ready_event: asyncio.Event - result: Optional[TunnelResult] = None - error: Optional[str] = None - udid: Optional[str] = None - - -_TUNNELS: Dict[str, TunnelState] = {} - -_orig_rsd_connect = RemoteServiceDiscoveryService.connect -_orig_remotexpc_connect = RemoteXPCConnection.connect -_orig_create_using_tcp = ServiceConnection.create_using_tcp - -# Global reference to keep the CLI process alive -_location_sim_process: Optional[asyncio.subprocess.Process] = None - -# Global DVT session for reuse -_dvt_session: Optional[DvtSecureSocketProxyService] = None -_dvt_session_lock = asyncio.Lock() -_current_rsd: Optional[RemoteServiceDiscoveryService] = None - -handler = logging.StreamHandler() -handler.setFormatter(JsonFormatter()) -root_logger = logging.getLogger() -root_logger.handlers = [handler] -root_logger.setLevel(logging.INFO) -logger = logging.getLogger("ios-api") - - -async def _remotexpc_connect_with_timeout(self): - """Wrapper to add timeout to TCP connection and handshake""" - logger.info(f"RemoteXPC attempting TCP connection to {self.address}") - tcp_start = time.time() - try: - self._reader, self._writer = await asyncio.wait_for( - asyncio.open_connection(self.address[0], self.address[1]), - timeout=5.0 - ) - tcp_elapsed = time.time() - tcp_start - logger.info(f"RemoteXPC TCP connected in {tcp_elapsed:.2f}s") - except asyncio.TimeoutError as exc: - tcp_elapsed = time.time() - tcp_start - logger.error(f"RemoteXPC TCP connection to {self.address} timed out after {tcp_elapsed:.2f}s") - raise asyncio.TimeoutError(f"TCP connection to {self.address} timed out after 5s") from exc - except Exception as exc: - tcp_elapsed = time.time() - tcp_start - logger.error(f"RemoteXPC TCP connection to {self.address} failed after {tcp_elapsed:.2f}s: {exc}") - raise - - logger.info(f"RemoteXPC starting handshake") - handshake_start = time.time() - try: - await self._do_handshake() - handshake_elapsed = time.time() - handshake_start - logger.info(f"RemoteXPC handshake complete in {handshake_elapsed:.2f}s") - except Exception as exc: - handshake_elapsed = time.time() - handshake_start - logger.error(f"RemoteXPC handshake failed after {handshake_elapsed:.2f}s: {exc}") - await self.close() - raise - - -async def _rsd_connect_with_timeout(self): - """Modified connect with faster timeout for lockdown connections""" - await self.service.connect() - try: - self.peer_info = await self.service.receive_response() - self.udid = self.peer_info["Properties"]["UniqueDeviceID"] - self.product_type = self.peer_info["Properties"]["ProductType"] - - # Skip lockdown connection - not working over RSD tunnel and not needed for DVT - self.lockdown = None - logger.info("Skipping lockdown connection for RSD (not required for DVT)") - - self.all_values = self.lockdown.all_values if self.lockdown is not None else {} - except Exception: - await self.close() - raise - - -RemoteXPCConnection.connect = _remotexpc_connect_with_timeout -RemoteServiceDiscoveryService.connect = _rsd_connect_with_timeout - - -def _create_using_tcp_with_ipv6( - hostname: str, - port: int, - keep_alive: bool = True, - create_connection_timeout: int = 5, # Reduced from 20 to 5 seconds -): - """Force IPv6 connection with reduced timeout""" - import socket as socket_module - from pymobiledevice3.osu.os_utils import get_os_utils - - # Force IPv6 socket creation for tunnel addresses - if ':' in hostname: # IPv6 address - logger.info(f"ServiceConnection connecting to {hostname}:{port} with {create_connection_timeout}s timeout") - sock = socket_module.socket(socket_module.AF_INET6, socket_module.SOCK_STREAM) - sock.settimeout(create_connection_timeout) - connect_start = time.time() - try: - sock.connect((hostname, port)) - connect_elapsed = time.time() - connect_start - logger.info(f"ServiceConnection connected to {hostname}:{port} in {connect_elapsed:.2f}s") - # Keep a 5-second timeout for subsequent operations for faster failure detection - sock.settimeout(5.0) - if keep_alive: - get_os_utils().set_keepalive(sock) - return ServiceConnection(sock) - except Exception as exc: - connect_elapsed = time.time() - connect_start - logger.error(f"ServiceConnection failed to {hostname}:{port} after {connect_elapsed:.2f}s: {exc}") - sock.close() - raise - else: - # Fall back to original for non-IPv6 - return _orig_create_using_tcp( - hostname, - port, - keep_alive=keep_alive, - create_connection_timeout=create_connection_timeout, - ) - - -ServiceConnection.create_using_tcp = staticmethod(_create_using_tcp_with_ipv6) - - -def _get_developer_mode_status() -> bool: - device_serial = _get_single_device_udid() - lockdown = create_using_usbmux(serial=device_serial, autopair=False) - return True if MobileImageMounterService(lockdown).query_developer_mode_status() == 1 else False - -def _get_developer_disk_image_status() -> bool: - device_serial = _get_single_device_udid() - lockdown = create_using_usbmux(serial=device_serial, autopair=False) - images = MobileImageMounterService(lockdown).copy_devices() - is_ddi = False - for image in images: - if image.get("DiskImageType") == "Personalized" and image.get("PersonalizedImageType") == "DeveloperDiskImage": - is_ddi = True - - return is_ddi - - - - return True if MobileImageMounterService(lockdown).is_image_mounted() == 1 else False - -def _get_single_device_udid() -> str: - devices = list_devices() - if not devices: - raise HTTPException(status_code=404, detail="No devices connected") - if len(devices) > 1: - raise HTTPException(status_code=400, detail="Multiple devices connected") - return devices[0].serial - - -def _parse_protocol(value: str) -> TunnelProtocol: - try: - return TunnelProtocol[value.upper()] - except KeyError as exc: - raise HTTPException(status_code=400, detail=f"Unsupported protocol: {value}") from exc - - -async def _run_usbmux_tunnel(udid: str, protocol: TunnelProtocol, state: TunnelState) -> None: - lockdown = None - proxy = None - try: - logger.info("Starting usbmux tunnel for udid=%s protocol=%s", udid, protocol.name.lower()) - lockdown = create_using_usbmux(serial=udid) if udid else create_using_usbmux() - proxy = await CoreDeviceTunnelProxy.create(lockdown) - async with start_tunnel(proxy, protocol=protocol) as tunnel: - state.result = tunnel - logger.info( - "Tunnel ready udid=%s address=%s port=%s interface=%s protocol=%s", - udid, - tunnel.address, - tunnel.port, - tunnel.interface, - tunnel.protocol.name.lower(), - ) - state.ready_event.set() - await state.stop_event.wait() - except Exception as e: - logger.exception("Tunnel failed udid=%s", udid) - state.error = str(e) - state.ready_event.set() - finally: - logger.info("Shutting down tunnel udid=%s", udid) - if proxy is not None: - await proxy.close() - if lockdown is not None: - lockdown.close() - - -async def _start_tunnel_internal( - udid: str, - protocol: TunnelProtocol, - wait_for_device: bool, - wait_timeout_seconds: int, -) -> TunnelResult: - key = "" - existing = _TUNNELS.get(key) - if existing and existing.task is not None and not existing.task.done(): - if existing.result is not None: - return existing.result - - if wait_for_device: - timeout = max(1, wait_timeout_seconds) - start = asyncio.get_event_loop().time() - while True: - devices = list_devices() - if len(devices) == 1: - udid = devices[0].serial - break - if asyncio.get_event_loop().time() - start > timeout: - raise HTTPException(status_code=504, detail="Timed out waiting for device") - await asyncio.sleep(0.5) - - stop_event = asyncio.Event() - ready_event = asyncio.Event() - state = TunnelState( - task=None, - stop_event=stop_event, - ready_event=ready_event, - udid=udid, - ) - _TUNNELS[key] = state - state.task = asyncio.create_task(_run_usbmux_tunnel(udid, protocol, state)) - - try: - await asyncio.wait_for(ready_event.wait(), timeout=15) - except asyncio.TimeoutError as exc: - raise HTTPException(status_code=504, detail="Timed out waiting for tunnel to start") from exc - - if state.error: - _TUNNELS.pop(key, None) - raise HTTPException(status_code=500, detail=f"Failed to start tunnel: {state.error}") - - return state.result - - -def _wait_for_port(address: str, port: int, timeout_seconds: float = 10.0, interval_seconds: float = 0.5) -> bool: - deadline = time.time() + timeout_seconds - while time.time() < deadline: - try: - sock = socket.create_connection((address, port), timeout=2) - sock.close() - return True - except OSError: - if time.time() >= deadline: - break - time.sleep(interval_seconds) - return False - - -async def _wait_for_port_async( - address: str, - port: int, - timeout_seconds: float = 10.0, - interval_seconds: float = 0.5, -) -> bool: - return await asyncio.to_thread(_wait_for_port, address, port, timeout_seconds, interval_seconds) - - -def _simulate_location_with_dvt(service_provider, latitude: float, longitude: float) -> None: - logger.info("DVT opening session") - - # For RSD connections, manually create the service connection to dtservicehub - # without going through lockdown (which doesn't work over RSD tunnel) - if isinstance(service_provider, RemoteServiceDiscoveryService): - from pymobiledevice3.services.remote_server import RemoteServer - - logger.info("Using RSD dtservicehub connection") - # Get raw TCP connection to dtservicehub without RSDCheckin - service = service_provider.start_lockdown_service_without_checkin( - DvtSecureSocketProxyService.RSD_SERVICE_NAME - ) - - # Manually create RemoteServer instance bypassing LockdownService.__init__ - dvt = RemoteServer.__new__(RemoteServer) - dvt.service_name = DvtSecureSocketProxyService.RSD_SERVICE_NAME - dvt.lockdown = service_provider - dvt.service = service - dvt.logger = logging.getLogger(DvtSecureSocketProxyService.__module__) - dvt.should_remove_ssl_context = False - dvt.channel_cache = {} - from pymobiledevice3.services.remote_server import ChannelFragmenter - dvt.channel_messages = {0: ChannelFragmenter()} # BROADCAST_CHANNEL = 0 - from pymobiledevice3.services.remote_server import Channel - dvt.broadcast = Channel.create(0, dvt) - import threading - dvt.lock = threading.Lock() - dvt.supported_identifiers = [] - else: - dvt = DvtSecureSocketProxyService(service_provider) - - try: - handshake_start = time.monotonic() - logger.info("DVT handshake start") - dvt.perform_handshake() - handshake_seconds = time.monotonic() - handshake_start - logger.info("DVT handshake complete in %.2fs", handshake_seconds) - - set_start = time.monotonic() - LocationSimulation(dvt).set(latitude, longitude) - set_seconds = time.monotonic() - set_start - logger.info("DVT location set sent in %.2fs", set_seconds) - finally: - if isinstance(service_provider, RemoteServiceDiscoveryService): - dvt.service.close() - - -async def _get_or_create_dvt_session(rsd: RemoteServiceDiscoveryService): - """Get existing DVT session or create a new one (with proper initialization)""" - global _dvt_session, _current_rsd - - async with _dvt_session_lock: - # If we have a session and it's for the same RSD connection, reuse it - if _dvt_session is not None and _current_rsd is rsd: - logger.info("Reusing existing DVT session") - return _dvt_session - - # Close old session if it exists - if _dvt_session is not None: - logger.info("Closing old DVT session") - try: - _dvt_session.service.close() - except: - pass - _dvt_session = None - _current_rsd = None - - # Create new DVT session - logger.info("Creating new DVT session") - - def _create_dvt(): - from pymobiledevice3.services.remote_server import RemoteServer - - # Get raw TCP connection to dtservicehub without RSDCheckin - service = rsd.start_lockdown_service_without_checkin( - DvtSecureSocketProxyService.RSD_SERVICE_NAME - ) - - # Manually create RemoteServer instance - dvt = RemoteServer.__new__(RemoteServer) - dvt.service_name = DvtSecureSocketProxyService.RSD_SERVICE_NAME - dvt.lockdown = rsd - dvt.service = service - dvt.logger = logging.getLogger(DvtSecureSocketProxyService.__module__) - dvt.should_remove_ssl_context = False - dvt.channel_cache = {} - from pymobiledevice3.services.remote_server import ChannelFragmenter - dvt.channel_messages = {0: ChannelFragmenter()} - from pymobiledevice3.services.remote_server import Channel - dvt.broadcast = Channel.create(0, dvt) - import threading - dvt.lock = threading.Lock() - dvt.supported_identifiers = [] - - # Perform handshake - logger.info("DVT handshake start") - handshake_start = time.monotonic() - dvt.perform_handshake() - handshake_seconds = time.monotonic() - handshake_start - logger.info("DVT handshake complete in %.2fs", handshake_seconds) - - return dvt - - _dvt_session = await asyncio.wait_for( - asyncio.to_thread(_create_dvt), - timeout=10.0 - ) - _current_rsd = rsd - - return _dvt_session - - -async def _simulate_location_via_library(rsd: RemoteServiceDiscoveryService, latitude: float, longitude: float) -> None: - """Use library with persistent DVT session to avoid location bounce""" - logger.info("Using library for location simulation (persistent session)") - - dvt = await _get_or_create_dvt_session(rsd) - - # Set location using the persistent session - def _set_location(): - set_start = time.monotonic() - LocationSimulation(dvt).set(latitude, longitude) - set_seconds = time.monotonic() - set_start - logger.info("DVT location set in %.2fs", set_seconds) - - await asyncio.to_thread(_set_location) - logger.info("Location updated successfully (session maintained)") - - -async def _simulate_location_via_cli(address: str, port: int, latitude: float, longitude: float) -> None: - """Use pymobiledevice3 CLI with GPX playback to avoid location bounce""" - global _location_sim_process - import tempfile - import xml.etree.ElementTree as ET - - logger.info("Using pymobiledevice3 CLI for location simulation (GPX playback)") - - # Kill any existing location simulation process - if _location_sim_process is not None: - logger.info("Stopping previous location simulation") - try: - _location_sim_process.terminate() - await asyncio.wait_for(_location_sim_process.wait(), timeout=2.0) - except: - try: - _location_sim_process.kill() - await _location_sim_process.wait() - except: - pass - _location_sim_process = None - - # Create a GPX file with a single stationary point that loops - # This prevents the location from bouncing back when we restart the process - gpx_content = f''' - - - Simulated Location - - - - - - - - - -''' - - # Write GPX to temporary file - gpx_file = tempfile.NamedTemporaryFile(mode='w', suffix='.gpx', delete=False) - gpx_file.write(gpx_content) - gpx_file.flush() - gpx_path = gpx_file.name - gpx_file.close() - - cmd = [ - ".venv/bin/pymobiledevice3", - "developer", - "dvt", - "simulate-location", - "play", - "--rsd", - address, - str(port), - "--disable-sleep", # Play immediately without delays - gpx_path, - ] - - logger.info(f"Running CLI command: {' '.join(cmd)}") - - # Start the process and keep it alive - _location_sim_process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.DEVNULL, - ) - - # Wait briefly to ensure location is set - await asyncio.sleep(1.5) - - # Check if process is still running - if _location_sim_process.returncode is not None: - error_msg = f"CLI process exited with code {_location_sim_process.returncode}" - logger.error(error_msg) - _location_sim_process = None - # Clean up temp file - try: - os.unlink(gpx_path) - except: - pass - raise HTTPException(status_code=500, detail=error_msg) - - logger.info("CLI location simulation started (GPX playback maintains location)") - - # Note: We don't clean up the GPX file here since the process needs it - # It will be cleaned up when the process is stopped - - -async def _simulate_location_via_rsd_async(address: str, port: int, latitude: float, longitude: float) -> None: - logger.info("Connecting to RSD address=%s port=%s", address, port) - port_check_start = time.monotonic() - if not await _wait_for_port_async(address, port, timeout_seconds=3, interval_seconds=0.5): - port_check_seconds = time.monotonic() - port_check_start - raise HTTPException( - status_code=504, - detail=f"RSD port unreachable after {port_check_seconds:.2f}s", - ) - port_check_seconds = time.monotonic() - port_check_start - logger.info("RSD port reachable in %.2fs", port_check_seconds) - rsd = RemoteServiceDiscoveryService((address, port)) - connect_timeout = 60 - max_attempts = 2 - for attempt in range(1, max_attempts + 1): - connect_start = time.monotonic() - try: - await asyncio.wait_for(rsd.connect(), timeout=connect_timeout) - connect_seconds = time.monotonic() - connect_start - logger.info("RSD connect complete in %.2fs on attempt %s/%s", connect_seconds, attempt, max_attempts) - break - except asyncio.TimeoutError as exc: - connect_seconds = time.monotonic() - connect_start - if attempt >= max_attempts: - raise HTTPException( - status_code=504, - detail=f"Timed out connecting to RSD after {max_attempts} attempts", - ) from exc - logger.warning( - "RSD connect timed out in %.2fs on attempt %s/%s; retrying", - connect_seconds, - attempt, - max_attempts, - ) - await asyncio.sleep(2) - try: - services = rsd.peer_info.get("Services", {}) - dtservicehub = services.get(DvtSecureSocketProxyService.RSD_SERVICE_NAME, {}) - logger.info("RSD services keys=%s", list(services.keys())) - logger.info("RSD dtservicehub entry=%s", dtservicehub) - dt_port = int(dtservicehub.get("Port", 0)) if dtservicehub else 0 - if dt_port: - logger.info("Waiting for RSD dtservicehub port address=%s port=%s", address, dt_port) - wait_start = time.monotonic() - if await _wait_for_port_async(address, dt_port, timeout_seconds=30, interval_seconds=0.5): - wait_seconds = time.monotonic() - wait_start - logger.info("RSD dtservicehub port reachable address=%s port=%s", address, dt_port) - else: - wait_seconds = time.monotonic() - wait_start - logger.warning("RSD dtservicehub port still unreachable address=%s port=%s", address, dt_port) - logger.info("RSD dtservicehub wait complete in %.2fs", wait_seconds) - else: - logger.warning("RSD dtservicehub missing or port=0") - logger.info("Starting DVT location set") - await asyncio.to_thread(_simulate_location_with_dvt, rsd, latitude, longitude) - finally: - await rsd.close() - - -def _simulate_location_via_rsd(address: str, port: int, latitude: float, longitude: float) -> None: - asyncio.run(_simulate_location_via_rsd_async(address, port, latitude, longitude)) - - -async def _preflight_rsd_async( - address: str, - port: int, - interface: Optional[str], - protocol: Optional[str], -) -> PreflightResponse: - rsd = RemoteServiceDiscoveryService((address, port)) - await rsd.connect() - try: - services = rsd.peer_info.get("Services", {}) - dtservicehub = services.get(DvtSecureSocketProxyService.RSD_SERVICE_NAME, {}) - dt_port = int(dtservicehub.get("Port", 0)) if dtservicehub else 0 - lockdown_trusted = services.get("com.apple.mobile.lockdown.remote.trusted", {}) - lockdown_untrusted = services.get("com.apple.mobile.lockdown.remote.untrusted", {}) - trusted_port = int(lockdown_trusted.get("Port", 0)) if lockdown_trusted else 0 - untrusted_port = int(lockdown_untrusted.get("Port", 0)) if lockdown_untrusted else 0 - - dt_reachable = ( - await _wait_for_port_async(address, dt_port, timeout_seconds=5, interval_seconds=0.5) if dt_port else False - ) - trusted_reachable = ( - await _wait_for_port_async(address, trusted_port, timeout_seconds=3, interval_seconds=0.5) - if trusted_port - else False - ) - untrusted_reachable = ( - await _wait_for_port_async(address, untrusted_port, timeout_seconds=3, interval_seconds=0.5) - if untrusted_port - else False - ) - - return PreflightResponse( - rsd_address=address, - rsd_port=port, - interface=interface, - protocol=protocol, - dtservicehub_port=dt_port or None, - dtservicehub_reachable=dt_reachable, - lockdown_trusted_port=trusted_port or None, - lockdown_untrusted_port=untrusted_port or None, - lockdown_trusted_reachable=trusted_reachable, - lockdown_untrusted_reachable=untrusted_reachable, - ) - finally: - await rsd.close() - - -app = FastAPI(title="iOS Device Management API") - - -@app.get("/api/status") -async def get_status(): - """Lists all devices visible to USBMux.""" - try: - devices = list_devices() - if len(devices) > 1: - device_list = [] - for d in devices: - device_list.append(DeviceShort(udid=d.serial, connection_type=d.connection_type)) - return DeviceStatus(device_connected=True, device_count=len(devices), devices=device_list) - try: - lockdown = create_using_usbmux(serial=devices[0].serial, autopair=False) - state = _TUNNELS.get("") - if state and state.result: - rsd_address = state.result.address - rsd_port = state.result.port - else: - udid = devices[0].serial - logger.info("Auto-starting tunnel") - result = await _start_tunnel_internal( - udid, - TunnelProtocol.TCP, - True, - 30, - ) - rsd_address = result.address - rsd_port = result.port - - return DeviceStatus(device_connected=True, device_count=len(devices), udid=devices[0].serial, - device_name=lockdown.get_value(key='DeviceName'), - product_version=lockdown.product_version, - phone_number=lockdown.get_value(key='PhoneNumber'), - developer_mode_enabled=_get_developer_mode_status(), - ddi_mounted=_get_developer_disk_image_status(), - rsd_address=rsd_address, rsd_port=rsd_port, ) - except Exception as e: - logger.info("Error establishing lockdown: %s", str(e)) - return DeviceStatus(device_connected=False, device_count=0) - except Exception as e: - logger.info("No device connected: %s", str(e)) - return DeviceStatus(device_connected=False, device_count=0) - - -@app.get("/api/lockdown/status") -async def get_lockdown_status(): - """Checks lockdown connectivity and basic device info.""" - try: - device_serial = _get_single_device_udid() - lockdown = create_using_usbmux(serial=device_serial, autopair=False) - return { - "udid": device_serial, - "product_version": lockdown.product_version, - "device_name": lockdown.get_value(key='DeviceName'), - "phone_number": lockdown.get_value(key='PhoneNumber'), - "status": "Connected" - } - except Exception as e: - return {"status": "Disconnected", "error": str(e)} - - -@app.post("/api/tunnel/start") -async def start_usb_tunnel(data: TunnelStartRequest): - """Starts a CoreDevice tunnel to a USB device and returns RSD connection details.""" - udid = _get_single_device_udid() - key = "" - - -@app.post("/api/tunnel/stop") -async def stop_usb_tunnel(): - """Stops a previously started tunnel.""" - key = "" - state = _TUNNELS.get(key) - if state is None: - raise HTTPException(status_code=404, detail="No tunnel found") - if state.task is None: - _TUNNELS.pop(key, None) - raise HTTPException(status_code=500, detail="Tunnel state is incomplete") - - logger.info("Stopping tunnel") - state.stop_event.set() - await state.task - _TUNNELS.pop(key, None) - return {"status": "stopped"} - - -@app.post("/api/tunnel/stop-all") -async def stop_all_tunnels(): - """Stops all running tunnels.""" - if not _TUNNELS: - return {"status": "stopped", "count": 0} - logger.info("Stopping all tunnels count=%s", len(_TUNNELS)) - items = list(_TUNNELS.items()) - for _, state in items: - if state.task is not None: - state.stop_event.set() - await asyncio.gather( - *[state.task for _, state in items if state.task is not None], - return_exceptions=True, - ) - _TUNNELS.clear() - return {"status": "stopped", "count": len(items)} - - -@app.get("/api/tunnel/status") -async def get_tunnel_status(): - """Returns the status of all active tunnels.""" - items = [] - for key, state in _TUNNELS.items(): - if state.result is None: - continue - if state.task is None: - continue - items.append( - { - "udid": state.udid, - "rsd_address": state.result.address, - "rsd_port": state.result.port, - "interface": state.result.interface, - "protocol": state.result.protocol.name.lower(), - "running": not state.task.done(), - } - ) - return {"tunnels": items} - - -@app.get("/api/preflight", response_model=PreflightResponse) -async def preflight(): - """Checks RSD connectivity and service port reachability.""" - state = _TUNNELS.get("") - if state is None or state.result is None: - udid = _get_single_device_udid() - logger.info("Auto-starting tunnel for preflight") - result = await _start_tunnel_internal( - udid, - TunnelProtocol.TCP, - True, - 30, - ) - address = result.address - port = result.port - return await _preflight_rsd_async( - address, - port, - result.interface, - result.protocol.name.lower(), - ) - address = state.result.address - port = state.result.port - return await _preflight_rsd_async( - address, +import random +import sys +import socketio +import tempfile +from contextlib import nullcontext +from functools import partial +from pathlib import Path +from typing import Annotated, Optional, TextIO + +import typer +from typer_injector import InjectingTyper + +from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remotepairing_manual_pairing +from pymobiledevice3.cli.cli_common import ( + RSDServiceProviderDep, + async_command, + print_json, + prompt_device_list, + sudo_required, + user_requested_colored_output, +) +from pymobiledevice3.common import get_home_folder +from pymobiledevice3.exceptions import NoDeviceConnectedError +from pymobiledevice3.pair_records import PAIRING_RECORD_EXT, get_remote_pairing_record_filename +from pymobiledevice3.remote.common import ConnectionType, TunnelProtocol +from pymobiledevice3.remote.module_imports import MAX_IDLE_TIMEOUT, start_tunnel, verify_tunnel_imports +from pymobiledevice3.remote.remote_service_discovery import RSD_PORT +from pymobiledevice3.remote.tunnel_service import ( + RemotePairingManualPairingService, + get_core_device_tunnel_services, + get_remote_pairing_tunnel_services, +) +from pymobiledevice3.remote.utils import get_rsds +from pymobiledevice3.tunneld.api import TUNNELD_DEFAULT_ADDRESS +from pymobiledevice3.utils import run_in_loop +from server import TunneldRunnerSio, LocationSimulationState, logger + + +def main(): + cli_tunneld(host="0.0.0.0", port=8000) + + +def cli_tunneld( + host: Annotated[str, typer.Option(help="Address to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[0], + port: Annotated[int, typer.Option(help="Port to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[1], + daemonize: Annotated[bool, typer.Option("--daemonize", "-d", help="Run tunneld in the background.")] = False, + protocol: Annotated[ + TunnelProtocol, + typer.Option( + "--protocol", + "-p", + case_sensitive=False, + help="Transport protocol for tunneld (default: TCP on Python >=3.13, otherwise QUIC).", + ), + ] = TunnelProtocol.DEFAULT, + usb: Annotated[bool, typer.Option(help="Enable USB monitoring")] = True, + wifi: Annotated[bool, typer.Option(help="Enable WiFi monitoring")] = True, + usbmux: Annotated[bool, typer.Option(help="Enable usbmux monitoring")] = True, + mobdev2: Annotated[bool, typer.Option(help="Enable mobdev2 monitoring")] = True, + context: Annotated[LocationSimulationState, typer.Option( + help="Location simulation context to use for the server.")] = LocationSimulationState(), +) -> None: + """Start Tunneld service for remote tunneling""" + if not verify_tunnel_imports(): + return + tunneld_runner = partial( + TunneldRunnerSio.create, + host, port, - state.result.interface, - state.result.protocol.name.lower(), + protocol=protocol, + usb_monitor=usb, + wifi_monitor=wifi, + usbmux_monitor=usbmux, + mobdev2_monitor=mobdev2, + context=context, ) - - -@app.post("/api/simulate-location/clear") -async def clear_location(): - """Stops location simulation by closing the DVT session.""" - global _location_sim_process, _dvt_session, _current_rsd - - logger.info("Clearing location simulation") - - # Close DVT session if it exists - async with _dvt_session_lock: - if _dvt_session is not None: - try: - # Clear the location first - def _clear(): - LocationSimulation(_dvt_session).clear() - - await asyncio.to_thread(_clear) - except: - pass - - try: - _dvt_session.service.close() - except: - pass - - _dvt_session = None - _current_rsd = None - - # Also kill CLI process if running - if _location_sim_process is not None: + if daemonize: try: - _location_sim_process.terminate() - await asyncio.wait_for(_location_sim_process.wait(), timeout=2.0) - except: - try: - _location_sim_process.kill() - await _location_sim_process.wait() - except: - pass - _location_sim_process = None - - logger.info("Location simulation cleared") - return {"status": "cleared"} - - -@app.post("/api/simulate-location") -async def set_location(data: LocationUpdate): - """Sets a simulated GPS location on the device.""" - try: - logger.info("Simulate location request lat=%s lon=%s", data.latitude, data.longitude) - rsd_address = data.rsd_address - rsd_port = data.rsd_port - - if rsd_address is None and rsd_port is None: - state = _TUNNELS.get("") - if state and state.result: - rsd_address = state.result.address - rsd_port = state.result.port - - if rsd_address is None and rsd_port is None: - udid = _get_single_device_udid() - logger.info("Auto-starting tunnel for simulate-location") - result = await _start_tunnel_internal( - udid, - TunnelProtocol.TCP, - True, - 30, - ) - rsd_address = result.address - rsd_port = result.port - - if rsd_address is not None and rsd_port is not None: - # Use CLI approach - library DVT handshake has unresolved issues - # Note: Location will briefly bounce back to real location when changing - await _simulate_location_via_cli(rsd_address, rsd_port, data.latitude, data.longitude) - else: - raise HTTPException(status_code=400, detail="RSD address/port required") - - logger.info("Simulate location success") - return {"status": "success", "location": {"lat": data.latitude, "lon": data.longitude}} - except HTTPException: - raise - except Exception as e: - logger.exception("Simulate location failed") - raise HTTPException(status_code=500, detail=f"Failed to set location: {str(e)}") + from daemonize import Daemonize + except ImportError as e: + raise NotImplementedError("daemonizing is only supported on unix platforms") from e + with tempfile.NamedTemporaryFile("wt") as pid_file: + daemon = Daemonize(app=f"Tunneld {host}:{port}", pid=pid_file.name, action=tunneld_runner) + logger.info(f"starting Tunneld {host}:{port}") + daemon.start() + else: + tunneld_runner() +# 4. Entry point (always last) if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d04ebdf..ef070ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,8 @@ requires-python = ">=3.14" dependencies = [ "fastapi==0.135.1", "pydantic==2.12.5", - "flask==3.1.3", - "flask-cors==6.0.2", - "pymobiledevice3==7.8.3", + "pymobiledevice3==9.0.0", + "python-socketio==5.16.1", "typing==3.10.0.0", "uvicorn==0.41.0", ] diff --git a/server.py b/server.py new file mode 100644 index 0000000..7fde647 --- /dev/null +++ b/server.py @@ -0,0 +1,568 @@ +import asyncio +import dataclasses +import json +import logging +import os +import signal +import traceback +import warnings +import fastapi +import random +from fastapi import FastAPI +from typing import Optional +import socketio +from contextlib import asynccontextmanager, suppress +from ssl import SSLEOFError +from typing import Optional, Union + +import construct + +from pymobiledevice3.bonjour import browse_remoted +from pymobiledevice3.cli.cli_common import print_json + +with warnings.catch_warnings(): + # Ignore: "Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater." + warnings.simplefilter("ignore", category=UserWarning) + import fastapi + +import uvicorn +from construct import StreamError +from fastapi import FastAPI +from packaging.version import Version + +from pymobiledevice3 import usbmux +from pymobiledevice3.exceptions import ( + ConnectionFailedError, + ConnectionFailedToUsbmuxdError, + ConnectionTerminatedError, + DeviceNotFoundError, + GetProhibitedError, + IncorrectModeError, + InvalidServiceError, + LockdownError, + MuxException, + PairingError, + QuicProtocolNotSupportedError, + StreamClosedError, + TunneldConnectionError +) +from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns +from pymobiledevice3.osu.os_utils import get_os_utils +from pymobiledevice3.remote.common import TunnelProtocol +from pymobiledevice3.remote.module_imports import start_tunnel +from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService +from pymobiledevice3.remote.tunnel_service import ( + CoreDeviceTunnelProxy, + RemotePairingProtocol, + TunnelResult, + create_core_device_tunnel_service_using_rsd, + get_remote_pairing_tunnel_services, +) +from pymobiledevice3.remote.utils import get_rsds, stop_remoted +from pymobiledevice3.utils import asyncio_print_traceback +from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation +from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider +from pymobiledevice3.tunneld.server import TunneldCore, TunnelTask +from pymobiledevice3.tunneld.api import ( + TUNNELD_DEFAULT_ADDRESS, + get_tunneld_device_by_udid, + get_tunneld_devices, +) + + +class JsonFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + payload = { + "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"), + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + if record.exc_info: + payload["exc_info"] = self.formatException(record.exc_info) + return json.dumps(payload, ensure_ascii=True) + + +handler = logging.StreamHandler() +handler.setFormatter(JsonFormatter()) +root_logger = logging.getLogger() +root_logger.handlers = [handler] +root_logger.setLevel(logging.INFO) +logger = logging.getLogger("ios-api") + +# bugfix: after the device reboots, it might take some time for remoted to start answering the bonjour queries +REATTEMPT_INTERVAL = 5 +REATTEMPT_COUNT = 5 + +REMOTEPAIRING_INTERVAL = 5 +MOBDEV2_INTERVAL = 5 + +# USB monitor will periodically forget what interfaces it has seen +# and force a full rescan. The value is number of iterations of the +# inner loop (which sleeps one second each) before blowing away the +# `previous_ips` cache. +USB_MONITOR_RESCAN_INTERVAL = 30 + +USBMUX_INTERVAL = 2 +OSUTILS = get_os_utils() +TUNNEL_ACQUIRE_TIMEOUT_SECONDS = 15 +DVT_CONNECT_TIMEOUT_SECONDS = 20 + + +class LocationSimulationState: + def __init__(self): + self.latitude: Optional[float] = None + self.longitude: Optional[float] = None + self.udid: Optional[str] = None + self.simulation_active: bool = False + self.queue: asyncio.Queue = asyncio.Queue() + self.simulation_task: Optional[asyncio.Task] = None + self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") + + +class TunneldRunnerSio: + """TunneldRunner orchestrate between the webserver and TunneldCore""" + + @classmethod + def create( + cls, + host: str, + port: int, + context: LocationSimulationState = LocationSimulationState(), + protocol: TunnelProtocol = TunnelProtocol.QUIC, + usb_monitor: bool = True, + wifi_monitor: bool = True, + usbmux_monitor: bool = True, + mobdev2_monitor: bool = True, + ) -> None: + cls( + host, + port, + protocol=protocol, + usb_monitor=usb_monitor, + wifi_monitor=wifi_monitor, + usbmux_monitor=usbmux_monitor, + mobdev2_monitor=mobdev2_monitor, + context=context, + )._run_app() + + def __init__( + self, + host: str, + port: int, + context: LocationSimulationState = LocationSimulationState(), + protocol: TunnelProtocol = TunnelProtocol.QUIC, + usb_monitor: bool = True, + wifi_monitor: bool = True, + usbmux_monitor: bool = True, + mobdev2_monitor: bool = True, + ): + @asynccontextmanager + async def lifespan(app: FastAPI): + self._tunneld_core.start() + yield + logger.info("Closing tunneld tasks...") + await empty_queue() + await self._tunneld_core.close() + await self.context.sio.shutdown() + + self.host = host + self.port = port + self.protocol = protocol + self.context = context + self._tunneld_api_address = ("127.0.0.1" if host in ("0.0.0.0", "::") else host, port) + self._app = FastAPI(title="iOS Device Management API", lifespan=lifespan, cors_allowed_origins="*") + self._asgi_app = socketio.ASGIApp(self.context.sio, self._app) + self._tunneld_core = TunneldCore( + protocol=protocol, + wifi_monitor=wifi_monitor, + usb_monitor=usb_monitor, + usbmux_monitor=usbmux_monitor, + mobdev2_monitor=mobdev2_monitor, + ) + + async def get_tun( + udid: Optional[str] = None, max_retries: int = 10, retry_delay: float = 0.5 + ) -> RemoteServiceDiscoveryService: + """Try to connect to tunneld with retries to handle startup delay.""" + for attempt in range(max_retries): + try: + if udid: + rsd = await get_tunneld_device_by_udid(udid, self._tunneld_api_address) + if rsd is not None: + logger.info("Connected to tunnel for udid %s after %s retries", udid, attempt) + return rsd + + rsds = await get_tunneld_devices(self._tunneld_api_address) + if rsds: + if udid: + logger.warning("Tunnel for udid %s not found; using first available tunnel", udid) + logger.info("Connected to tunneld after %s retries", attempt) + return rsds[0] + except TunneldConnectionError: + if attempt < max_retries - 1: + logger.info("Waiting for tunneld to be ready... (attempt %s/%s)", attempt + 1, max_retries) + await asyncio.sleep(retry_delay) + else: + logger.error("Failed to connect to tunneld after max retries") + raise + raise TunneldConnectionError() + + async def empty_queue(): + """Empties all items from an asyncio.Queue.""" + logger.info("Clearing location simulation queue... resetting ios location") + q = self.context.queue + while not q.empty(): + try: + q.get_nowait() + q.task_done() + await q.join() + except asyncio.QueueEmpty: + break + tun = await get_tun(self.context.udid) + if tun is not None: + async with DvtProvider(tun) as dvt, LocationSimulationQueue(dvt, self.context) as locate_simulation: + await locate_simulation.clear() + self.context.simulation_active = False + + async def start_queue(): + logger.info("Starting location simulation worker...") + self.context.simulation_active = True + try: + if self.context.udid is None: + active_udids = sorted( + {t.udid for t in self._tunneld_core.tunnel_tasks.values() if t.udid is not None and t.tunnel is not None} + ) + if len(active_udids) == 1: + self.context.udid = active_udids[0] + logger.info("Simulation worker: auto-selected udid=%s from active tunnel", self.context.udid) + elif len(active_udids) == 0: + logger.error("Simulation worker: no active tunnel with udid available") + await self.context.sio.emit( + "error", + {"type": "simulation_no_tunnel", "message": "No active tunnel found. Start tunnel first."}, + namespace="/", + ) + return + else: + logger.error("Simulation worker: multiple active tunnels; explicit udid required: %s", active_udids) + await self.context.sio.emit( + "error", + { + "type": "simulation_udid_required", + "message": "Multiple active tunnels found; provide udid in start_simulate_location.", + "udids": active_udids, + }, + namespace="/", + ) + return + + logger.info("Simulation worker: acquiring tunnel (udid=%s)", self.context.udid) + tun = await asyncio.wait_for( + get_tun(self.context.udid), + timeout=TUNNEL_ACQUIRE_TIMEOUT_SECONDS, + ) + logger.info("Simulation worker: tunnel acquired, connecting DVT provider") + dvt_provider = DvtProvider(tun) + dvt = await asyncio.wait_for(dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS) + logger.info("Simulation worker: DVT provider connected, starting queue playback") + try: + async with LocationSimulationQueue(dvt, self.context) as locate_simulation: + await locate_simulation.play_queue() + finally: + logger.info("Simulation worker: closing DVT provider") + await dvt_provider.__aexit__(None, None, None) + logger.info("Simulation worker: DVT provider closed") + except TimeoutError: + logger.error( + "Simulation worker timeout. tunnel_timeout=%ss dvt_timeout=%ss udid=%s", + TUNNEL_ACQUIRE_TIMEOUT_SECONDS, + DVT_CONNECT_TIMEOUT_SECONDS, + self.context.udid, + ) + await self.context.sio.emit( + "error", + { + "type": "simulation_timeout", + "udid": self.context.udid, + "tunnel_timeout_seconds": TUNNEL_ACQUIRE_TIMEOUT_SECONDS, + "dvt_timeout_seconds": DVT_CONNECT_TIMEOUT_SECONDS, + }, + namespace="/", + ) + except Exception: + logger.exception("Simulation worker crashed") + await self.context.sio.emit( + "error", + {"type": "simulation_crash", "udid": self.context.udid}, + namespace="/", + ) + finally: + self.context.simulation_active = False + self.context.simulation_task = None + + def iterate_multidim(d): + mydict = {} + for key, value in d.items(): + if isinstance(value, dict): + iterate_multidim(value) + elif isinstance(value, str) or isinstance(value, int) or isinstance(value, float) or isinstance(value, bool) or isinstance(value, list): + mydict[key] = value + else: + mydict[key] = '' + return mydict + + @self._app.get("/") + @self.context.sio.event + async def list_tunnels() -> dict[str, list[dict]]: + """Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}""" + tunnels = {} + for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items(): + if (active_tunnel.udid is None) or (active_tunnel.tunnel is None): + continue + if active_tunnel.udid not in tunnels: + tunnels[active_tunnel.udid] = [] + tunnels[active_tunnel.udid].append({ + "tunnel-address": active_tunnel.tunnel.address, + "tunnel-port": active_tunnel.tunnel.port, + "interface": ip, + }) + return tunnels + + @self._app.get("/device_info") + async def device_info(): + """Get device information""" + tunnels = {} + for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items(): + if (active_tunnel.udid is None) or (active_tunnel.tunnel is None): + continue + if active_tunnel.udid not in tunnels: + tunnels[active_tunnel.udid] = {} + try: + lockdown = await create_using_usbmux(serial=active_tunnel.udid, autopair=False) + tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values) + except Exception as e: + logger.error(f"Failed to create lockdown session for device {active_tunnel.udid}: {e}") + continue + return tunnels + + @self._app.get("/shutdown") + async def shutdown() -> fastapi.Response: + """Shutdown Tunneld""" + os.kill(os.getpid(), signal.SIGINT) + data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."} + return generate_http_response(data) + + @self._app.get("/clear_tunnels") + async def clear_tunnels() -> fastapi.Response: + self._tunneld_core.clear() + data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."} + return generate_http_response(data) + + @self._app.get("/cancel") + async def cancel_tunnel(udid: str) -> fastapi.Response: + self._tunneld_core.cancel(udid=udid) + data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."} + return generate_http_response(data) + + @self._app.get("/hello") + async def hello() -> fastapi.Response: + data = {"message": "Hello, I'm alive"} + return generate_http_response(data) + + def generate_http_response( + data: dict, status_code: int = 200, media_type: str = "application/json" + ) -> fastapi.Response: + return fastapi.Response(status_code=status_code, media_type=media_type, content=json.dumps(data)) + + @self._app.get("/start-tunnel") + @self.context.sio.event + async def start_tunnel( + udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None + ) -> fastapi.Response: + udid_tunnels = [ + t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid and t.tunnel is not None + ] + if len(udid_tunnels) > 0: + self.context.udid = udid + data = { + "interface": udid_tunnels[0].interface, + "port": udid_tunnels[0].port, + "address": udid_tunnels[0].address, + } + return generate_http_response(data) + + queue = asyncio.Queue() + created_task = False + + try: + if not created_task and connection_type in ("usbmux", None): + task_identifier = f"usbmux-{udid}" + try: + async with await create_using_usbmux(udid) as lockdown: + service = await CoreDeviceTunnelProxy.create(lockdown) + task = asyncio.create_task( + self._tunneld_core.start_tunnel_task( + task_identifier, service, protocol=TunnelProtocol.TCP, queue=queue + ), + name=f"start-tunnel-task-{task_identifier}", + ) + self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid) + created_task = True + except (ConnectionFailedError, InvalidServiceError, MuxException): + pass + if connection_type in ("usb", None): + for rsd in await get_rsds(udid=udid): + rsd_ip = rsd.service.address[0] + if ip is not None and rsd_ip != ip: + await rsd.close() + continue + task = asyncio.create_task( + self._tunneld_core.start_tunnel_task( + rsd_ip, await create_core_device_tunnel_service_using_rsd(rsd), queue=queue + ), + name=f"start-tunnel-usb-{rsd_ip}", + ) + self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid) + created_task = True + if not created_task and connection_type in ("wifi", None): + for remotepairing in await get_remote_pairing_tunnel_services(udid=udid): + remotepairing_ip = remotepairing.hostname + if ip is not None and remotepairing_ip != ip: + await remotepairing.close() + continue + task = asyncio.create_task( + self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue), + name=f"start-tunnel-wifi-{remotepairing_ip}", + ) + self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask( + task=task, udid=remotepairing.remote_identifier + ) + created_task = True + except Exception as e: + return fastapi.Response( + status_code=501, + content=json.dumps({ + "error": { + "exception": e.__class__.__name__, + "traceback": traceback.format_exc(), + } + }), + ) + + if not created_task: + return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"})) + + tunnel: Optional[TunnelResult] = await queue.get() + if tunnel is not None: + self.context.udid = udid + data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address} + return generate_http_response(data) + else: + return fastapi.Response( + status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"}) + ) + + @self.context.sio.event + async def connect(sid, environ): + logger.info("Client connected: %s", sid) + await self.context.sio.emit("connect", sid, namespace="/") + + @self.context.sio.event + async def request_update(sid, data): + logger.info("Update request from %s", sid) + + # await self.context.sio.emit("status_update", await get_status()) + + @self.context.sio.event + async def message(sid, data): + logger.info("Received message from %s: %s", data, sid) + await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/") + + @self.context.sio.event + async def simulate_location(sid, data): + logger.info("Simulate location request from %s: %s", sid, data) + latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None) + longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None) + delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0) + if latitude is not None and longitude is not None: + logger.info("Adding location %s, %s with %ss delay to the queue", latitude, longitude, delay) + await self.context.queue.put((latitude, longitude, delay)) + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, + "data": {"latitude": self.context.latitude, + "cur_longitude": longitude, "next_move": delay}}, + namespace="/") + else: + logger.warning("Invalid location data received from %s: %s", sid, data) + await self.context.sio.emit("error", "Invalid location data", namespace="/") + + @self.context.sio.event + async def start_simulate_location(sid, data): + logger.info("Start location simulation request from %s", sid) + if isinstance(data, dict) and data.get("udid"): + self.context.udid = data["udid"] + if self.context.simulation_task is None or self.context.simulation_task.done(): + self.context.simulation_active = True + self.context.simulation_task = asyncio.create_task( + start_queue(), + name="location-simulation-worker", + ) + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None}, + namespace="/") + + @self.context.sio.event + async def end_simulate_location(sid, data): + logger.info("End location simulation request from %s", sid) + if self.context.simulation_task is not None and not self.context.simulation_task.done(): + await self.context.queue.put((None, None, None)) + with suppress(asyncio.CancelledError): + await self.context.simulation_task + await empty_queue() + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None}, + namespace="/") + + @self.context.sio.event + async def disconnect(sid): + logger.info("Client disconnected: %s", sid) + + @self.context.sio.event + async def start_tunneld(sid, data): + logger.info("Start tunneld request from %s: %s", sid, data) + try: + self._tunneld_core.start() + logger.info("Tunneld started successfully") + except Exception as e: + logger.error("Error starting tunneld: %s", e) + + def _run_app(self) -> None: + uvicorn.run(self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1) + + +class LocationSimulationQueue(LocationSimulation): + def __init__(self, dvt, context: LocationSimulationState): + super().__init__(dvt) + self.context = context + + async def play_queue(self, disable_sleep: bool = False, timing_randomness_range: int = 0) -> None: + while True: + latitude, longitude, delay = await self.context.queue.get() + if (latitude, longitude, delay) == (None, None, None): + break + if delay > 0 and not disable_sleep: + if timing_randomness_range > 0: + delay = delay + random.uniform(-timing_randomness_range, timing_randomness_range) + for i in range(delay, 0, -1): + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, + "data": {"latitude": self.context.latitude, + "longitude": self.context.longitude, + "next_move": i}}, namespace="/") + await asyncio.sleep(1) + await self.set(latitude, longitude) + self.context.latitude = latitude + self.context.longitude = longitude + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, + "data": {"latitude": self.context.latitude, + "longitude": self.context.longitude, + "next_move": None}}, namespace="/") + logger.info("Set simulated location to %s, %s after %ss delay", latitude, longitude, delay) + self.context.queue.task_done() diff --git a/socktest.py b/socktest.py new file mode 100644 index 0000000..d344f39 --- /dev/null +++ b/socktest.py @@ -0,0 +1,75 @@ +import socketio +from fastapi import FastAPI +from fastapi.responses import HTMLResponse + +# 1. Create the FastAPI app +app = FastAPI() + +# 2. Create the Socket.IO server (Async mode for FastAPI) +sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*') + +# 3. Wrap FastAPI with Socket.IO +# This allows 'socket_app' to handle socket traffic and pass +# everything else to 'app' +socket_app = socketio.ASGIApp(sio, app) + +# --- Socket.IO Events --- + +@sio.event +async def connect(sid, environ): + print(f"Client connected: {sid}") + await sio.emit('response', {'data': 'Connected to Server!'}) + +@sio.event +async def message(sid, data): + print(f"Received from {sid}: {data}") + # Broadcast to everyone (including sender) + await sio.emit('response', {'data': f"Echo: {data}"}) + +@sio.event +async def disconnect(sid): + print(f"Client disconnected: {sid}") + +# --- FastAPI Routes --- + +html_client = """ + + + + FastAPI Socket.IO Test + + + + + +

Socket.IO Test

+

Disconnected

+ + +
    + + +""" + +@app.get("/") +async def index(): + return HTMLResponse(html_client) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 0b420cb..00b54ba 100644 --- a/uv.lock +++ b/uv.lock @@ -94,10 +94,9 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "fastapi" }, - { name = "flask" }, - { name = "flask-cors" }, { name = "pydantic" }, { name = "pymobiledevice3" }, + { name = "python-socketio" }, { name = "typing" }, { name = "uvicorn" }, ] @@ -105,14 +104,22 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastapi", specifier = "==0.135.1" }, - { name = "flask", specifier = "==3.1.3" }, - { name = "flask-cors", specifier = "==6.0.2" }, { name = "pydantic", specifier = "==2.12.5" }, - { name = "pymobiledevice3", specifier = "==7.8.3" }, + { name = "pymobiledevice3", specifier = "==9.0.0" }, + { name = "python-socketio", specifier = "==5.16.1" }, { name = "typing", specifier = "==3.10.0.0" }, { name = "uvicorn", specifier = "==0.41.0" }, ] +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, +] + [[package]] name = "blessed" version = "1.32.0" @@ -126,15 +133,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/47/de8f185a1f537fdb5117fcde7050472b8cde3561179e9a68e1a566a6e6c6/blessed-1.32.0-py3-none-any.whl", hash = "sha256:c6fdc18838491ebc7f0460234917eff4e172074934f5f80e82672417bd74be70", size = 111172, upload-time = "2026-02-28T20:58:58.59Z" }, ] -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - [[package]] name = "bpylist2" version = "4.1.1" @@ -346,6 +344,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "developer-disk-image" version = "0.2.0" @@ -405,36 +412,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] -[[package]] -name = "flask" -version = "3.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, -] - -[[package]] -name = "flask-cors" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, -] - [[package]] name = "gpxpy" version = "1.6.2" @@ -564,15 +541,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - [[package]] name = "jedi" version = "0.19.2" @@ -585,18 +553,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - [[package]] name = "jinxed" version = "1.3.0" @@ -654,36 +610,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -705,15 +631,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "nest-asyncio" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, -] - [[package]] name = "opack2" version = "0.0.1" @@ -1020,7 +937,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/9ae75ede398b7adf5 [[package]] name = "pymobiledevice3" -version = "7.8.3" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asn1" }, @@ -1031,6 +948,7 @@ dependencies = [ { name = "construct-typing" }, { name = "cryptography" }, { name = "daemonize" }, + { name = "defusedxml" }, { name = "developer-disk-image" }, { name = "fastapi" }, { name = "gpxpy" }, @@ -1040,7 +958,6 @@ dependencies = [ { name = "inquirer3" }, { name = "ipsw-parser" }, { name = "ipython" }, - { name = "nest-asyncio" }, { name = "opack2" }, { name = "packaging" }, { name = "parameter-decorators" }, @@ -1066,9 +983,9 @@ dependencies = [ { name = "wsproto" }, { name = "xonsh" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/16/6290b69b2ab04b01184536d93a605305749e09e2056ecacc768ec600bb3f/pymobiledevice3-7.8.3.tar.gz", hash = "sha256:3d88af218dea373249318412a08de999978a3a8f502d5929e2cd8e7820e8c6c6", size = 658504, upload-time = "2026-03-04T07:16:50.629Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/89/aed5abbb6a4ece29bed2b9e85ccfbc28a8197658a4923e8cd2d5193796d8/pymobiledevice3-9.0.0.tar.gz", hash = "sha256:e85c169d67cf17d1dcf4ce26e3a84a801e86d13a0144ff7fb57eb532745ddcfb", size = 735101, upload-time = "2026-03-11T08:37:05.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/51/e83eeab57581319bbce7e7516c871cf9bb07cb7aabe93f70b18831875701/pymobiledevice3-7.8.3-py3-none-any.whl", hash = "sha256:93aa53611ebfe7a10819661abe16b7a83b02134cfbc4cbb7dc5ad5b4c0e3bd7b", size = 709133, upload-time = "2026-03-04T07:16:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ae/aaff1375b383c78729b6b25abac1458e9fd5c0c84b8da364bf204559dc16/pymobiledevice3-9.0.0-py3-none-any.whl", hash = "sha256:7366533cc8807299ef0b88c6c56a77120f7d984914ec6436191a7cc2991d3ae7", size = 789609, upload-time = "2026-03-11T08:37:01.731Z" }, ] [[package]] @@ -1092,6 +1009,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-engineio" +version = "4.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, +] + [[package]] name = "python-pcapng" version = "2.1.1" @@ -1101,6 +1030,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/a4/141a5fbb51c8e3dee10445d02785d44f8a66a150af6916b4e1776e5065c6/python_pcapng-2.1.1-py3-none-any.whl", hash = "sha256:2c83e9f9f60d61cbb6c86f80fa9e3d722f1bb606a59a64a96d6ba0179d97ffcf", size = 33503, upload-time = "2022-08-23T18:59:08.754Z" }, ] +[[package]] +name = "python-socketio" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, +] + [[package]] name = "pytun-pmd3" version = "3.0.3" @@ -1244,6 +1186,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1430,18 +1384,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] -[[package]] -name = "werkzeug" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, -] - [[package]] name = "wsproto" version = "1.3.2"