pymobiledevice3 4.14.6__py3-none-any.whl → 7.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- misc/plist_sniffer.py +15 -15
- misc/remotexpc_sniffer.py +29 -28
- misc/understanding_idevice_protocol_layers.md +15 -10
- pymobiledevice3/__main__.py +317 -127
- pymobiledevice3/_version.py +22 -4
- pymobiledevice3/bonjour.py +358 -113
- pymobiledevice3/ca.py +253 -16
- pymobiledevice3/cli/activation.py +31 -23
- pymobiledevice3/cli/afc.py +49 -40
- pymobiledevice3/cli/amfi.py +16 -21
- pymobiledevice3/cli/apps.py +87 -42
- pymobiledevice3/cli/backup.py +160 -90
- pymobiledevice3/cli/bonjour.py +44 -40
- pymobiledevice3/cli/cli_common.py +204 -198
- pymobiledevice3/cli/companion_proxy.py +14 -14
- pymobiledevice3/cli/crash.py +105 -56
- pymobiledevice3/cli/developer/__init__.py +62 -0
- pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
- pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
- pymobiledevice3/cli/developer/arbitration.py +50 -0
- pymobiledevice3/cli/developer/condition.py +33 -0
- pymobiledevice3/cli/developer/core_device.py +294 -0
- pymobiledevice3/cli/developer/debugserver.py +244 -0
- pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
- pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
- pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
- pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
- pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
- pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
- pymobiledevice3/cli/developer/simulate_location.py +51 -0
- pymobiledevice3/cli/diagnostics/__init__.py +75 -0
- pymobiledevice3/cli/diagnostics/battery.py +47 -0
- pymobiledevice3/cli/idam.py +42 -0
- pymobiledevice3/cli/lockdown.py +108 -103
- pymobiledevice3/cli/mounter.py +158 -99
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +45 -24
- pymobiledevice3/cli/power_assertion.py +18 -17
- pymobiledevice3/cli/processes.py +17 -23
- pymobiledevice3/cli/profile.py +165 -109
- pymobiledevice3/cli/provision.py +35 -34
- pymobiledevice3/cli/remote.py +217 -129
- pymobiledevice3/cli/restore.py +159 -143
- pymobiledevice3/cli/springboard.py +63 -53
- pymobiledevice3/cli/syslog.py +193 -86
- pymobiledevice3/cli/usbmux.py +73 -33
- pymobiledevice3/cli/version.py +5 -7
- pymobiledevice3/cli/webinspector.py +376 -214
- pymobiledevice3/common.py +3 -1
- pymobiledevice3/exceptions.py +182 -58
- pymobiledevice3/irecv.py +52 -53
- pymobiledevice3/irecv_devices.py +1489 -464
- pymobiledevice3/lockdown.py +473 -275
- pymobiledevice3/lockdown_service_provider.py +15 -8
- pymobiledevice3/osu/os_utils.py +27 -9
- pymobiledevice3/osu/posix_util.py +34 -15
- pymobiledevice3/osu/win_util.py +14 -8
- pymobiledevice3/pair_records.py +102 -21
- pymobiledevice3/remote/common.py +8 -4
- pymobiledevice3/remote/core_device/app_service.py +94 -67
- pymobiledevice3/remote/core_device/core_device_service.py +17 -14
- pymobiledevice3/remote/core_device/device_info.py +5 -5
- pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
- pymobiledevice3/remote/core_device/file_service.py +53 -23
- pymobiledevice3/remote/remote_service_discovery.py +79 -45
- pymobiledevice3/remote/remotexpc.py +73 -44
- pymobiledevice3/remote/tunnel_service.py +442 -317
- pymobiledevice3/remote/utils.py +14 -13
- pymobiledevice3/remote/xpc_message.py +145 -125
- pymobiledevice3/resources/dsc_uuid_map.py +19 -19
- pymobiledevice3/resources/firmware_notifications.py +20 -16
- pymobiledevice3/resources/notifications.txt +144 -0
- pymobiledevice3/restore/asr.py +27 -27
- pymobiledevice3/restore/base_restore.py +110 -21
- pymobiledevice3/restore/consts.py +87 -66
- pymobiledevice3/restore/device.py +59 -12
- pymobiledevice3/restore/fdr.py +46 -48
- pymobiledevice3/restore/ftab.py +19 -19
- pymobiledevice3/restore/img4.py +163 -0
- pymobiledevice3/restore/mbn.py +587 -0
- pymobiledevice3/restore/recovery.py +151 -151
- pymobiledevice3/restore/restore.py +562 -544
- pymobiledevice3/restore/restore_options.py +131 -110
- pymobiledevice3/restore/restored_client.py +51 -31
- pymobiledevice3/restore/tss.py +385 -267
- pymobiledevice3/service_connection.py +252 -59
- pymobiledevice3/services/accessibilityaudit.py +202 -120
- pymobiledevice3/services/afc.py +962 -365
- pymobiledevice3/services/amfi.py +24 -30
- pymobiledevice3/services/companion.py +23 -19
- pymobiledevice3/services/crash_reports.py +71 -47
- pymobiledevice3/services/debugserver_applist.py +3 -3
- pymobiledevice3/services/device_arbitration.py +8 -8
- pymobiledevice3/services/device_link.py +101 -79
- pymobiledevice3/services/diagnostics.py +973 -967
- pymobiledevice3/services/dtfetchsymbols.py +8 -8
- pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
- pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
- pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
- pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
- pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
- pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
- pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
- pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
- pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
- pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
- pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
- pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
- pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
- pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
- pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
- pymobiledevice3/services/file_relay.py +10 -10
- pymobiledevice3/services/heartbeat.py +9 -8
- pymobiledevice3/services/house_arrest.py +16 -15
- pymobiledevice3/services/idam.py +20 -0
- pymobiledevice3/services/installation_proxy.py +173 -81
- pymobiledevice3/services/lockdown_service.py +20 -10
- pymobiledevice3/services/misagent.py +22 -19
- pymobiledevice3/services/mobile_activation.py +147 -64
- pymobiledevice3/services/mobile_config.py +331 -294
- pymobiledevice3/services/mobile_image_mounter.py +141 -113
- pymobiledevice3/services/mobilebackup2.py +203 -145
- pymobiledevice3/services/notification_proxy.py +11 -11
- pymobiledevice3/services/os_trace.py +134 -74
- pymobiledevice3/services/pcapd.py +314 -302
- pymobiledevice3/services/power_assertion.py +10 -9
- pymobiledevice3/services/preboard.py +4 -4
- pymobiledevice3/services/remote_fetch_symbols.py +21 -14
- pymobiledevice3/services/remote_server.py +176 -146
- pymobiledevice3/services/restore_service.py +16 -16
- pymobiledevice3/services/screenshot.py +15 -12
- pymobiledevice3/services/simulate_location.py +7 -7
- pymobiledevice3/services/springboard.py +15 -15
- pymobiledevice3/services/syslog.py +5 -5
- pymobiledevice3/services/web_protocol/alert.py +11 -11
- pymobiledevice3/services/web_protocol/automation_session.py +251 -239
- pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
- pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
- pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
- pymobiledevice3/services/web_protocol/driver.py +114 -111
- pymobiledevice3/services/web_protocol/element.py +124 -111
- pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
- pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
- pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
- pymobiledevice3/services/web_protocol/switch_to.py +30 -27
- pymobiledevice3/services/webinspector.py +189 -155
- pymobiledevice3/tcp_forwarder.py +87 -69
- pymobiledevice3/tunneld/__init__.py +0 -0
- pymobiledevice3/tunneld/api.py +63 -0
- pymobiledevice3/tunneld/server.py +603 -0
- pymobiledevice3/usbmux.py +198 -147
- pymobiledevice3/utils.py +14 -11
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
- pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
- pymobiledevice3/cli/developer.py +0 -1215
- pymobiledevice3/cli/diagnostics.py +0 -99
- pymobiledevice3/tunneld.py +0 -524
- pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import traceback
|
|
8
|
+
import warnings
|
|
9
|
+
from contextlib import asynccontextmanager, suppress
|
|
10
|
+
from ssl import SSLEOFError
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
|
|
13
|
+
import construct
|
|
14
|
+
|
|
15
|
+
from pymobiledevice3.bonjour import browse_remoted
|
|
16
|
+
|
|
17
|
+
with warnings.catch_warnings():
|
|
18
|
+
# Ignore: "Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater."
|
|
19
|
+
warnings.simplefilter("ignore", category=UserWarning)
|
|
20
|
+
import fastapi
|
|
21
|
+
|
|
22
|
+
import uvicorn
|
|
23
|
+
from construct import StreamError
|
|
24
|
+
from fastapi import FastAPI
|
|
25
|
+
from packaging.version import Version
|
|
26
|
+
|
|
27
|
+
from pymobiledevice3 import usbmux
|
|
28
|
+
from pymobiledevice3.exceptions import (
|
|
29
|
+
ConnectionFailedError,
|
|
30
|
+
ConnectionFailedToUsbmuxdError,
|
|
31
|
+
DeviceNotFoundError,
|
|
32
|
+
GetProhibitedError,
|
|
33
|
+
IncorrectModeError,
|
|
34
|
+
InvalidServiceError,
|
|
35
|
+
LockdownError,
|
|
36
|
+
MuxException,
|
|
37
|
+
PairingError,
|
|
38
|
+
StreamClosedError,
|
|
39
|
+
)
|
|
40
|
+
from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
|
|
41
|
+
from pymobiledevice3.osu.os_utils import get_os_utils
|
|
42
|
+
from pymobiledevice3.remote.common import TunnelProtocol
|
|
43
|
+
from pymobiledevice3.remote.module_imports import start_tunnel
|
|
44
|
+
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService
|
|
45
|
+
from pymobiledevice3.remote.tunnel_service import (
|
|
46
|
+
CoreDeviceTunnelProxy,
|
|
47
|
+
RemotePairingProtocol,
|
|
48
|
+
TunnelResult,
|
|
49
|
+
create_core_device_tunnel_service_using_rsd,
|
|
50
|
+
get_remote_pairing_tunnel_services,
|
|
51
|
+
)
|
|
52
|
+
from pymobiledevice3.remote.utils import get_rsds, stop_remoted
|
|
53
|
+
from pymobiledevice3.utils import asyncio_print_traceback
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
# bugfix: after the device reboots, it might take some time for remoted to start answering the bonjour queries
|
|
58
|
+
REATTEMPT_INTERVAL = 5
|
|
59
|
+
REATTEMPT_COUNT = 5
|
|
60
|
+
|
|
61
|
+
REMOTEPAIRING_INTERVAL = 5
|
|
62
|
+
MOBDEV2_INTERVAL = 5
|
|
63
|
+
|
|
64
|
+
USBMUX_INTERVAL = 2
|
|
65
|
+
OSUTILS = get_os_utils()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclasses.dataclass
|
|
69
|
+
class TunnelTask:
|
|
70
|
+
task: asyncio.Task
|
|
71
|
+
udid: Optional[str] = None
|
|
72
|
+
tunnel: Optional[TunnelResult] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TunneldCore:
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
protocol: TunnelProtocol = TunnelProtocol.DEFAULT,
|
|
79
|
+
wifi_monitor: bool = True,
|
|
80
|
+
usb_monitor: bool = True,
|
|
81
|
+
usbmux_monitor: bool = True,
|
|
82
|
+
mobdev2_monitor: bool = True,
|
|
83
|
+
) -> None:
|
|
84
|
+
self.protocol = protocol
|
|
85
|
+
self.tasks: list[asyncio.Task] = []
|
|
86
|
+
self.tunnel_tasks: dict[str, TunnelTask] = {}
|
|
87
|
+
self.usb_monitor = usb_monitor
|
|
88
|
+
self.wifi_monitor = wifi_monitor
|
|
89
|
+
self.usbmux_monitor = usbmux_monitor
|
|
90
|
+
self.mobdev2_monitor = mobdev2_monitor
|
|
91
|
+
|
|
92
|
+
def start(self) -> None:
|
|
93
|
+
"""Register all tasks"""
|
|
94
|
+
self.tasks = []
|
|
95
|
+
if self.usb_monitor:
|
|
96
|
+
self.tasks.append(asyncio.create_task(self.monitor_usb_task(), name="monitor-usb-task"))
|
|
97
|
+
if self.wifi_monitor:
|
|
98
|
+
self.tasks.append(asyncio.create_task(self.monitor_wifi_task(), name="monitor-wifi-task"))
|
|
99
|
+
if self.usbmux_monitor:
|
|
100
|
+
self.tasks.append(asyncio.create_task(self.monitor_usbmux_task(), name="monitor-usbmux-task"))
|
|
101
|
+
if self.mobdev2_monitor:
|
|
102
|
+
self.tasks.append(asyncio.create_task(self.monitor_mobdev2_task(), name="monitor-mobdev2-task"))
|
|
103
|
+
|
|
104
|
+
def tunnel_exists_for_udid(self, udid: str) -> bool:
|
|
105
|
+
for task in self.tunnel_tasks.values():
|
|
106
|
+
# Linux implementations of `usbmuxd` may report an incorrect value of UDID, dismissing the `-` character.
|
|
107
|
+
# For such cases, we also check for a UDID without it.
|
|
108
|
+
# See: <https://github.com/doronz88/pymobiledevice3/issues/1388#issuecomment-2782249770>
|
|
109
|
+
task_udid = task.udid or ""
|
|
110
|
+
if ((task_udid == udid) or (task_udid.replace("-", "") == udid)) and (task.tunnel is not None):
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
@asyncio_print_traceback
|
|
116
|
+
async def monitor_usb_task(self) -> None:
|
|
117
|
+
try:
|
|
118
|
+
previous_ips = []
|
|
119
|
+
while True:
|
|
120
|
+
current_ips = OSUTILS.get_ipv6_ips()
|
|
121
|
+
added = [ip for ip in current_ips if ip not in previous_ips]
|
|
122
|
+
removed = [ip for ip in previous_ips if ip not in current_ips]
|
|
123
|
+
|
|
124
|
+
previous_ips = current_ips
|
|
125
|
+
|
|
126
|
+
# logger.debug(f'added interfaces: {added}')
|
|
127
|
+
# logger.debug(f'removed interfaces: {removed}')
|
|
128
|
+
|
|
129
|
+
for ip in removed:
|
|
130
|
+
if ip in self.tunnel_tasks:
|
|
131
|
+
self.tunnel_tasks[ip].task.cancel()
|
|
132
|
+
with suppress(asyncio.CancelledError):
|
|
133
|
+
await self.tunnel_tasks[ip].task
|
|
134
|
+
|
|
135
|
+
if added:
|
|
136
|
+
# A new interface was attached
|
|
137
|
+
for answer in await browse_remoted():
|
|
138
|
+
for address in answer.addresses:
|
|
139
|
+
if address.iface.startswith("utun"):
|
|
140
|
+
# Skip already established tunnels
|
|
141
|
+
continue
|
|
142
|
+
if address.full_ip in self.tunnel_tasks:
|
|
143
|
+
# Skip already established tunnels
|
|
144
|
+
continue
|
|
145
|
+
self.tunnel_tasks[address.full_ip] = TunnelTask(
|
|
146
|
+
task=asyncio.create_task(
|
|
147
|
+
self.handle_new_potential_usb_cdc_ncm_interface_task(address.full_ip),
|
|
148
|
+
name=f"handle-new-potential-usb-cdc-ncm-interface-task-{address.full_ip}",
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# wait before re-iterating
|
|
153
|
+
await asyncio.sleep(1)
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@asyncio_print_traceback
|
|
158
|
+
async def monitor_wifi_task(self) -> None:
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
try:
|
|
162
|
+
remote_pairing_tunnel_services = await get_remote_pairing_tunnel_services()
|
|
163
|
+
for service in remote_pairing_tunnel_services:
|
|
164
|
+
if service.hostname in self.tunnel_tasks:
|
|
165
|
+
# skip tunnel if already exists for this ip
|
|
166
|
+
await service.close()
|
|
167
|
+
continue
|
|
168
|
+
if self.tunnel_exists_for_udid(service.remote_identifier):
|
|
169
|
+
# skip tunnel if already exists for this udid
|
|
170
|
+
await service.close()
|
|
171
|
+
continue
|
|
172
|
+
self.tunnel_tasks[service.hostname] = TunnelTask(
|
|
173
|
+
task=asyncio.create_task(
|
|
174
|
+
self.start_tunnel_task(service.hostname, service),
|
|
175
|
+
name=f"start-tunnel-task-wifi-{service.hostname}",
|
|
176
|
+
),
|
|
177
|
+
udid=service.remote_identifier,
|
|
178
|
+
)
|
|
179
|
+
except asyncio.exceptions.IncompleteReadError:
|
|
180
|
+
continue
|
|
181
|
+
except asyncio.CancelledError:
|
|
182
|
+
# Raise and cancel gracefully
|
|
183
|
+
raise
|
|
184
|
+
except Exception:
|
|
185
|
+
logger.exception(f"Got exception from {asyncio.current_task().get_name()}")
|
|
186
|
+
continue
|
|
187
|
+
await asyncio.sleep(REMOTEPAIRING_INTERVAL)
|
|
188
|
+
except asyncio.CancelledError:
|
|
189
|
+
# Cancel gracefully
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
@asyncio_print_traceback
|
|
193
|
+
async def monitor_usbmux_task(self) -> None:
|
|
194
|
+
try:
|
|
195
|
+
while True:
|
|
196
|
+
try:
|
|
197
|
+
for mux_device in usbmux.list_devices():
|
|
198
|
+
task_identifier = f"usbmux-{mux_device.serial}-{mux_device.connection_type}"
|
|
199
|
+
if self.tunnel_exists_for_udid(mux_device.serial):
|
|
200
|
+
# Skip if already established a tunnel for this udid
|
|
201
|
+
continue
|
|
202
|
+
if task_identifier in self.tunnel_tasks:
|
|
203
|
+
# Skip if already trying to establish a tunnel for this device
|
|
204
|
+
continue
|
|
205
|
+
service = None
|
|
206
|
+
try:
|
|
207
|
+
with create_using_usbmux(mux_device.serial) as lockdown:
|
|
208
|
+
service = await CoreDeviceTunnelProxy.create(lockdown)
|
|
209
|
+
except (
|
|
210
|
+
MuxException,
|
|
211
|
+
InvalidServiceError,
|
|
212
|
+
GetProhibitedError,
|
|
213
|
+
construct.core.StreamError,
|
|
214
|
+
ConnectionAbortedError,
|
|
215
|
+
DeviceNotFoundError,
|
|
216
|
+
LockdownError,
|
|
217
|
+
IncorrectModeError,
|
|
218
|
+
SSLEOFError,
|
|
219
|
+
):
|
|
220
|
+
if service is not None:
|
|
221
|
+
await service.close()
|
|
222
|
+
continue
|
|
223
|
+
self.tunnel_tasks[task_identifier] = TunnelTask(
|
|
224
|
+
udid=mux_device.serial,
|
|
225
|
+
task=asyncio.create_task(
|
|
226
|
+
self.start_tunnel_task(task_identifier, service, protocol=TunnelProtocol.TCP),
|
|
227
|
+
name=f"start-tunnel-task-{task_identifier}",
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
except ConnectionFailedToUsbmuxdError:
|
|
231
|
+
# This is exception is expected to occur repeatedly on linux running usbmuxd
|
|
232
|
+
# as long as there isn't any physical iDevice connected
|
|
233
|
+
logger.debug("failed to connect to usbmux. waiting for it to restart")
|
|
234
|
+
finally:
|
|
235
|
+
await asyncio.sleep(USBMUX_INTERVAL)
|
|
236
|
+
except asyncio.CancelledError:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
@asyncio_print_traceback
|
|
240
|
+
async def monitor_mobdev2_task(self) -> None:
|
|
241
|
+
try:
|
|
242
|
+
while True:
|
|
243
|
+
async for ip, lockdown in get_mobdev2_lockdowns(only_paired=True):
|
|
244
|
+
with lockdown:
|
|
245
|
+
udid = lockdown.udid
|
|
246
|
+
task_identifier = f"mobdev2-{udid}-{ip}"
|
|
247
|
+
if self.tunnel_exists_for_udid(udid):
|
|
248
|
+
# Skip tunnel if already exists for this udid
|
|
249
|
+
continue
|
|
250
|
+
if task_identifier in self.tunnel_tasks:
|
|
251
|
+
# Skip if already trying to establish a tunnel for this device
|
|
252
|
+
continue
|
|
253
|
+
try:
|
|
254
|
+
tunnel_service = await CoreDeviceTunnelProxy.create(lockdown)
|
|
255
|
+
except InvalidServiceError:
|
|
256
|
+
logger.warning(f"[{task_identifier}] failed to start CoreDeviceTunnelProxy - skipping")
|
|
257
|
+
continue
|
|
258
|
+
self.tunnel_tasks[task_identifier] = TunnelTask(
|
|
259
|
+
task=asyncio.create_task(
|
|
260
|
+
self.start_tunnel_task(task_identifier, tunnel_service),
|
|
261
|
+
name=f"start-tunnel-task-{task_identifier}",
|
|
262
|
+
),
|
|
263
|
+
udid=udid,
|
|
264
|
+
)
|
|
265
|
+
await asyncio.sleep(MOBDEV2_INTERVAL)
|
|
266
|
+
except asyncio.CancelledError:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
@asyncio_print_traceback
|
|
270
|
+
async def start_tunnel_task(
|
|
271
|
+
self,
|
|
272
|
+
task_identifier: str,
|
|
273
|
+
protocol_handler: Union[RemotePairingProtocol, CoreDeviceTunnelProxy],
|
|
274
|
+
queue: Optional[asyncio.Queue] = None,
|
|
275
|
+
protocol: Optional[TunnelProtocol] = None,
|
|
276
|
+
) -> None:
|
|
277
|
+
if protocol is None:
|
|
278
|
+
protocol = self.protocol
|
|
279
|
+
if isinstance(protocol_handler, CoreDeviceTunnelProxy):
|
|
280
|
+
protocol = TunnelProtocol.TCP
|
|
281
|
+
tun = None
|
|
282
|
+
bailed_out = False
|
|
283
|
+
try:
|
|
284
|
+
if self.tunnel_exists_for_udid(protocol_handler.remote_identifier):
|
|
285
|
+
# cancel current tunnel creation
|
|
286
|
+
raise asyncio.CancelledError()
|
|
287
|
+
|
|
288
|
+
async with start_tunnel(protocol_handler, protocol=protocol) as tun:
|
|
289
|
+
if not self.tunnel_exists_for_udid(protocol_handler.remote_identifier):
|
|
290
|
+
self.tunnel_tasks[task_identifier].tunnel = tun
|
|
291
|
+
self.tunnel_tasks[task_identifier].udid = protocol_handler.remote_identifier
|
|
292
|
+
if queue is not None:
|
|
293
|
+
queue.put_nowait(tun)
|
|
294
|
+
# avoid sending another message if succeeded
|
|
295
|
+
queue = None
|
|
296
|
+
logger.info(f"[{asyncio.current_task().get_name()}] Created tunnel --rsd {tun.address} {tun.port}")
|
|
297
|
+
await tun.client.wait_closed()
|
|
298
|
+
else:
|
|
299
|
+
bailed_out = True
|
|
300
|
+
logger.debug(
|
|
301
|
+
f"[{asyncio.current_task().get_name()}] Not establishing tunnel since there is already an "
|
|
302
|
+
f"active one for same udid"
|
|
303
|
+
)
|
|
304
|
+
except asyncio.CancelledError:
|
|
305
|
+
pass
|
|
306
|
+
except (
|
|
307
|
+
asyncio.exceptions.IncompleteReadError,
|
|
308
|
+
TimeoutError,
|
|
309
|
+
OSError,
|
|
310
|
+
ConnectionResetError,
|
|
311
|
+
StreamError,
|
|
312
|
+
InvalidServiceError,
|
|
313
|
+
) as e:
|
|
314
|
+
if tun is None:
|
|
315
|
+
logger.debug(f"Got {e.__class__.__name__} from {asyncio.current_task().get_name()}")
|
|
316
|
+
else:
|
|
317
|
+
logger.debug(f"Got {e.__class__.__name__} from tunnel --rsd {tun.address} {tun.port}")
|
|
318
|
+
except Exception:
|
|
319
|
+
logger.exception(f"Got exception from {asyncio.current_task().get_name()}")
|
|
320
|
+
finally:
|
|
321
|
+
if queue is not None:
|
|
322
|
+
# notify something went wrong
|
|
323
|
+
queue.put_nowait(None)
|
|
324
|
+
|
|
325
|
+
if tun is not None and not bailed_out:
|
|
326
|
+
logger.info(f"Disconnected from tunnel --rsd {tun.address} {tun.port}")
|
|
327
|
+
await tun.client.stop_tunnel()
|
|
328
|
+
|
|
329
|
+
if protocol_handler is not None:
|
|
330
|
+
with suppress(OSError):
|
|
331
|
+
await protocol_handler.close()
|
|
332
|
+
|
|
333
|
+
if task_identifier in self.tunnel_tasks:
|
|
334
|
+
# in case the tunnel was removed just now
|
|
335
|
+
self.tunnel_tasks.pop(task_identifier)
|
|
336
|
+
|
|
337
|
+
@asyncio_print_traceback
|
|
338
|
+
async def handle_new_potential_usb_cdc_ncm_interface_task(self, ip: str) -> None:
|
|
339
|
+
rsd = None
|
|
340
|
+
try:
|
|
341
|
+
# establish an untrusted RSD handshake
|
|
342
|
+
rsd = RemoteServiceDiscoveryService((ip, RSD_PORT))
|
|
343
|
+
|
|
344
|
+
with stop_remoted():
|
|
345
|
+
first_time = True
|
|
346
|
+
retry = False
|
|
347
|
+
while retry or first_time:
|
|
348
|
+
retry = False
|
|
349
|
+
try:
|
|
350
|
+
await rsd.connect()
|
|
351
|
+
except StreamClosedError:
|
|
352
|
+
# Could be on first try because of remoted race
|
|
353
|
+
if first_time:
|
|
354
|
+
retry = True
|
|
355
|
+
except (ConnectionRefusedError, TimeoutError, OSError) as e:
|
|
356
|
+
raise asyncio.CancelledError() from e
|
|
357
|
+
finally:
|
|
358
|
+
first_time = False
|
|
359
|
+
|
|
360
|
+
if (self.protocol == TunnelProtocol.QUIC) and (Version(rsd.product_version) < Version("17.0.0")):
|
|
361
|
+
await rsd.close()
|
|
362
|
+
rsd = None
|
|
363
|
+
raise asyncio.CancelledError()
|
|
364
|
+
|
|
365
|
+
await asyncio.create_task(
|
|
366
|
+
self.start_tunnel_task(ip, await create_core_device_tunnel_service_using_rsd(rsd)),
|
|
367
|
+
name=f"start-tunnel-task-usb-{ip}",
|
|
368
|
+
)
|
|
369
|
+
except asyncio.CancelledError:
|
|
370
|
+
pass
|
|
371
|
+
except PairingError:
|
|
372
|
+
logger.exception(f"Failed to pair with {ip}")
|
|
373
|
+
except RuntimeError:
|
|
374
|
+
logger.debug(f"Got RuntimeError from: {asyncio.current_task().get_name()}")
|
|
375
|
+
except Exception:
|
|
376
|
+
logger.exception(f"Error raised from: {asyncio.current_task().get_name()}: {traceback.format_exc()}")
|
|
377
|
+
finally:
|
|
378
|
+
if rsd is not None:
|
|
379
|
+
with suppress(OSError):
|
|
380
|
+
await rsd.close()
|
|
381
|
+
|
|
382
|
+
if ip in self.tunnel_tasks:
|
|
383
|
+
# In case the tunnel was removed just now
|
|
384
|
+
self.tunnel_tasks.pop(ip)
|
|
385
|
+
|
|
386
|
+
async def close(self) -> None:
|
|
387
|
+
"""close all tasks"""
|
|
388
|
+
for task in self.tasks + [tunnel_task.task for tunnel_task in self.tunnel_tasks.values()]:
|
|
389
|
+
task.cancel()
|
|
390
|
+
with suppress(asyncio.CancelledError):
|
|
391
|
+
await task
|
|
392
|
+
|
|
393
|
+
def get_tunnels_ips(self) -> dict:
|
|
394
|
+
"""Retrieve the available tunnel tasks and format them as {UDID: [IP]}"""
|
|
395
|
+
tunnels_ips = {}
|
|
396
|
+
for ip, active_tunnel in self.tunnel_tasks.items():
|
|
397
|
+
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
|
398
|
+
continue
|
|
399
|
+
if active_tunnel.udid not in tunnels_ips:
|
|
400
|
+
tunnels_ips[active_tunnel.udid] = [ip]
|
|
401
|
+
else:
|
|
402
|
+
tunnels_ips[active_tunnel.udid].append(ip)
|
|
403
|
+
return tunnels_ips
|
|
404
|
+
|
|
405
|
+
def cancel(self, udid: str) -> None:
|
|
406
|
+
"""Cancel active tunnels"""
|
|
407
|
+
for tunnel_ip in self.get_tunnels_ips().get(udid, []):
|
|
408
|
+
self.tunnel_tasks.pop(tunnel_ip).task.cancel()
|
|
409
|
+
logger.info(f"Canceling tunnel {tunnel_ip}")
|
|
410
|
+
|
|
411
|
+
def clear(self) -> None:
|
|
412
|
+
"""Clear active tunnels"""
|
|
413
|
+
for _udid, tunnel in self.tunnel_tasks.items():
|
|
414
|
+
logger.info(f"Removing tunnel {tunnel}")
|
|
415
|
+
tunnel.task.cancel()
|
|
416
|
+
self.tunnel_tasks = {}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class TunneldRunner:
|
|
420
|
+
"""TunneldRunner orchestrate between the webserver and TunneldCore"""
|
|
421
|
+
|
|
422
|
+
@classmethod
|
|
423
|
+
def create(
|
|
424
|
+
cls,
|
|
425
|
+
host: str,
|
|
426
|
+
port: int,
|
|
427
|
+
protocol: TunnelProtocol = TunnelProtocol.QUIC,
|
|
428
|
+
usb_monitor: bool = True,
|
|
429
|
+
wifi_monitor: bool = True,
|
|
430
|
+
usbmux_monitor: bool = True,
|
|
431
|
+
mobdev2_monitor: bool = True,
|
|
432
|
+
) -> None:
|
|
433
|
+
cls(
|
|
434
|
+
host,
|
|
435
|
+
port,
|
|
436
|
+
protocol=protocol,
|
|
437
|
+
usb_monitor=usb_monitor,
|
|
438
|
+
wifi_monitor=wifi_monitor,
|
|
439
|
+
usbmux_monitor=usbmux_monitor,
|
|
440
|
+
mobdev2_monitor=mobdev2_monitor,
|
|
441
|
+
)._run_app()
|
|
442
|
+
|
|
443
|
+
def __init__(
|
|
444
|
+
self,
|
|
445
|
+
host: str,
|
|
446
|
+
port: int,
|
|
447
|
+
protocol: TunnelProtocol = TunnelProtocol.QUIC,
|
|
448
|
+
usb_monitor: bool = True,
|
|
449
|
+
wifi_monitor: bool = True,
|
|
450
|
+
usbmux_monitor: bool = True,
|
|
451
|
+
mobdev2_monitor: bool = True,
|
|
452
|
+
):
|
|
453
|
+
@asynccontextmanager
|
|
454
|
+
async def lifespan(app: FastAPI):
|
|
455
|
+
self._tunneld_core.start()
|
|
456
|
+
yield
|
|
457
|
+
logger.info("Closing tunneld tasks...")
|
|
458
|
+
await self._tunneld_core.close()
|
|
459
|
+
|
|
460
|
+
self.host = host
|
|
461
|
+
self.port = port
|
|
462
|
+
self.protocol = protocol
|
|
463
|
+
self._app = FastAPI(lifespan=lifespan)
|
|
464
|
+
self._tunneld_core = TunneldCore(
|
|
465
|
+
protocol=protocol,
|
|
466
|
+
wifi_monitor=wifi_monitor,
|
|
467
|
+
usb_monitor=usb_monitor,
|
|
468
|
+
usbmux_monitor=usbmux_monitor,
|
|
469
|
+
mobdev2_monitor=mobdev2_monitor,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
@self._app.get("/")
|
|
473
|
+
async def list_tunnels() -> dict[str, list[dict]]:
|
|
474
|
+
"""Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
|
|
475
|
+
tunnels = {}
|
|
476
|
+
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
|
|
477
|
+
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
|
478
|
+
continue
|
|
479
|
+
if active_tunnel.udid not in tunnels:
|
|
480
|
+
tunnels[active_tunnel.udid] = []
|
|
481
|
+
tunnels[active_tunnel.udid].append({
|
|
482
|
+
"tunnel-address": active_tunnel.tunnel.address,
|
|
483
|
+
"tunnel-port": active_tunnel.tunnel.port,
|
|
484
|
+
"interface": ip,
|
|
485
|
+
})
|
|
486
|
+
return tunnels
|
|
487
|
+
|
|
488
|
+
@self._app.get("/shutdown")
|
|
489
|
+
async def shutdown() -> fastapi.Response:
|
|
490
|
+
"""Shutdown Tunneld"""
|
|
491
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
492
|
+
data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."}
|
|
493
|
+
return generate_http_response(data)
|
|
494
|
+
|
|
495
|
+
@self._app.get("/clear_tunnels")
|
|
496
|
+
async def clear_tunnels() -> fastapi.Response:
|
|
497
|
+
self._tunneld_core.clear()
|
|
498
|
+
data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."}
|
|
499
|
+
return generate_http_response(data)
|
|
500
|
+
|
|
501
|
+
@self._app.get("/cancel")
|
|
502
|
+
async def cancel_tunnel(udid: str) -> fastapi.Response:
|
|
503
|
+
self._tunneld_core.cancel(udid=udid)
|
|
504
|
+
data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."}
|
|
505
|
+
return generate_http_response(data)
|
|
506
|
+
|
|
507
|
+
@self._app.get("/hello")
|
|
508
|
+
async def hello() -> fastapi.Response:
|
|
509
|
+
data = {"message": "Hello, I'm alive"}
|
|
510
|
+
return generate_http_response(data)
|
|
511
|
+
|
|
512
|
+
def generate_http_response(
|
|
513
|
+
data: dict, status_code: int = 200, media_type: str = "application/json"
|
|
514
|
+
) -> fastapi.Response:
|
|
515
|
+
return fastapi.Response(status_code=status_code, media_type=media_type, content=json.dumps(data))
|
|
516
|
+
|
|
517
|
+
@self._app.get("/start-tunnel")
|
|
518
|
+
async def start_tunnel(
|
|
519
|
+
udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None
|
|
520
|
+
) -> fastapi.Response:
|
|
521
|
+
udid_tunnels = [
|
|
522
|
+
t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid and t.tunnel is not None
|
|
523
|
+
]
|
|
524
|
+
if len(udid_tunnels) > 0:
|
|
525
|
+
data = {
|
|
526
|
+
"interface": udid_tunnels[0].interface,
|
|
527
|
+
"port": udid_tunnels[0].port,
|
|
528
|
+
"address": udid_tunnels[0].address,
|
|
529
|
+
}
|
|
530
|
+
return generate_http_response(data)
|
|
531
|
+
|
|
532
|
+
queue = asyncio.Queue()
|
|
533
|
+
created_task = False
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
if not created_task and connection_type in ("usbmux", None):
|
|
537
|
+
task_identifier = f"usbmux-{udid}"
|
|
538
|
+
try:
|
|
539
|
+
with create_using_usbmux(udid) as lockdown:
|
|
540
|
+
service = await CoreDeviceTunnelProxy.create(lockdown)
|
|
541
|
+
task = asyncio.create_task(
|
|
542
|
+
self._tunneld_core.start_tunnel_task(
|
|
543
|
+
task_identifier, service, protocol=TunnelProtocol.TCP, queue=queue
|
|
544
|
+
),
|
|
545
|
+
name=f"start-tunnel-task-{task_identifier}",
|
|
546
|
+
)
|
|
547
|
+
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid)
|
|
548
|
+
created_task = True
|
|
549
|
+
except (ConnectionFailedError, InvalidServiceError, MuxException):
|
|
550
|
+
pass
|
|
551
|
+
if connection_type in ("usb", None):
|
|
552
|
+
for rsd in await get_rsds(udid=udid):
|
|
553
|
+
rsd_ip = rsd.service.address[0]
|
|
554
|
+
if ip is not None and rsd_ip != ip:
|
|
555
|
+
await rsd.close()
|
|
556
|
+
continue
|
|
557
|
+
task = asyncio.create_task(
|
|
558
|
+
self._tunneld_core.start_tunnel_task(
|
|
559
|
+
rsd_ip, await create_core_device_tunnel_service_using_rsd(rsd), queue=queue
|
|
560
|
+
),
|
|
561
|
+
name=f"start-tunnel-usb-{rsd_ip}",
|
|
562
|
+
)
|
|
563
|
+
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid)
|
|
564
|
+
created_task = True
|
|
565
|
+
if not created_task and connection_type in ("wifi", None):
|
|
566
|
+
for remotepairing in await get_remote_pairing_tunnel_services(udid=udid):
|
|
567
|
+
remotepairing_ip = remotepairing.hostname
|
|
568
|
+
if ip is not None and remotepairing_ip != ip:
|
|
569
|
+
await remotepairing.close()
|
|
570
|
+
continue
|
|
571
|
+
task = asyncio.create_task(
|
|
572
|
+
self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue),
|
|
573
|
+
name=f"start-tunnel-wifi-{remotepairing_ip}",
|
|
574
|
+
)
|
|
575
|
+
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
|
|
576
|
+
task=task, udid=remotepairing.remote_identifier
|
|
577
|
+
)
|
|
578
|
+
created_task = True
|
|
579
|
+
except Exception as e:
|
|
580
|
+
return fastapi.Response(
|
|
581
|
+
status_code=501,
|
|
582
|
+
content=json.dumps({
|
|
583
|
+
"error": {
|
|
584
|
+
"exception": e.__class__.__name__,
|
|
585
|
+
"traceback": traceback.format_exc(),
|
|
586
|
+
}
|
|
587
|
+
}),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if not created_task:
|
|
591
|
+
return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"}))
|
|
592
|
+
|
|
593
|
+
tunnel: Optional[TunnelResult] = await queue.get()
|
|
594
|
+
if tunnel is not None:
|
|
595
|
+
data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address}
|
|
596
|
+
return generate_http_response(data)
|
|
597
|
+
else:
|
|
598
|
+
return fastapi.Response(
|
|
599
|
+
status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"})
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def _run_app(self) -> None:
|
|
603
|
+
uvicorn.run(self._app, host=self.host, port=self.port, loop="asyncio")
|