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