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