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
pymobiledevice3/bonjour.py
CHANGED
|
@@ -1,145 +1,379 @@
|
|
|
1
|
+
# Async, dependency-light mDNS browser returning dataclasses with per-address interface names.
|
|
2
|
+
# Works for any DNS-SD type, e.g. "_remoted._tcp.local."
|
|
3
|
+
# - Uses ifaddr (optional) to map IPs -> local interfaces; otherwise iface will be None.
|
|
4
|
+
|
|
1
5
|
import asyncio
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
import contextlib
|
|
7
|
+
import ipaddress
|
|
8
|
+
import socket
|
|
9
|
+
import struct
|
|
10
|
+
import sys
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from dataclasses import dataclass, field
|
|
5
13
|
from typing import Optional
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
from zeroconf import IPVersion, ServiceListener, ServiceStateChange, Zeroconf
|
|
9
|
-
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
|
|
15
|
+
import ifaddr # pip install ifaddr
|
|
10
16
|
|
|
11
17
|
from pymobiledevice3.osu.os_utils import get_os_utils
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
REMOTEPAIRING_SERVICE_NAME = "_remotepairing._tcp.local."
|
|
20
|
+
REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAME = "_remotepairing-manual-pairing._tcp.local."
|
|
21
|
+
MOBDEV2_SERVICE_NAME = "_apple-mobdev2._tcp.local."
|
|
22
|
+
REMOTED_SERVICE_NAME = "_remoted._tcp.local."
|
|
17
23
|
OSUTILS = get_os_utils()
|
|
18
24
|
DEFAULT_BONJOUR_TIMEOUT = OSUTILS.bonjour_timeout
|
|
19
25
|
|
|
26
|
+
MDNS_PORT = 5353
|
|
27
|
+
MDNS_MCAST_V4 = "224.0.0.251"
|
|
28
|
+
MDNS_MCAST_V6 = "ff02::fb"
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
port: int
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class BonjourListener(ServiceListener):
|
|
30
|
-
def __init__(self, ip: str):
|
|
31
|
-
super().__init__()
|
|
32
|
-
self.name: Optional[str] = None
|
|
33
|
-
self.properties: dict[bytes, bytes] = {}
|
|
34
|
-
self.ip = ip
|
|
35
|
-
self.port: Optional[int] = None
|
|
36
|
-
self.addresses: list[str] = []
|
|
37
|
-
self.queue: asyncio.Queue = asyncio.Queue()
|
|
38
|
-
self.querying_task: Optional[asyncio.Task] = asyncio.create_task(self.query_addresses())
|
|
39
|
-
|
|
40
|
-
def async_on_service_state_change(
|
|
41
|
-
self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> None:
|
|
42
|
-
self.queue.put_nowait((zeroconf, service_type, name, state_change))
|
|
43
|
-
|
|
44
|
-
async def query_addresses(self) -> None:
|
|
45
|
-
zeroconf, service_type, name, state_change = await self.queue.get()
|
|
46
|
-
self.name = name
|
|
47
|
-
service_info = AsyncServiceInfo(service_type, name)
|
|
48
|
-
await service_info.async_request(zeroconf, 3000)
|
|
49
|
-
ipv4 = [inet_ntop(AF_INET, address.packed) for address in
|
|
50
|
-
service_info.ip_addresses_by_version(IPVersion.V4Only)]
|
|
51
|
-
ipv6 = []
|
|
52
|
-
if '%' in self.ip:
|
|
53
|
-
ipv6 = [inet_ntop(AF_INET6, address.packed) + '%' + self.ip.split('%')[1] for address in
|
|
54
|
-
service_info.ip_addresses_by_version(IPVersion.V6Only)]
|
|
55
|
-
self.addresses = ipv4 + ipv6
|
|
56
|
-
self.properties = service_info.properties
|
|
57
|
-
self.port = service_info.port
|
|
58
|
-
|
|
59
|
-
async def close(self) -> None:
|
|
60
|
-
self.querying_task.cancel()
|
|
61
|
-
try:
|
|
62
|
-
await self.querying_task
|
|
63
|
-
except asyncio.CancelledError:
|
|
64
|
-
pass
|
|
30
|
+
QTYPE_A = 1
|
|
31
|
+
QTYPE_PTR = 12
|
|
32
|
+
QTYPE_TXT = 16
|
|
33
|
+
QTYPE_AAAA = 28
|
|
34
|
+
QTYPE_SRV = 33
|
|
65
35
|
|
|
36
|
+
CLASS_IN = 0x0001
|
|
37
|
+
CLASS_QU = 0x8000 # unicast-response bit (we use multicast queries)
|
|
66
38
|
|
|
67
|
-
@dataclasses.dataclass
|
|
68
|
-
class BonjourQuery:
|
|
69
|
-
zc: AsyncZeroconf
|
|
70
|
-
service_browser: AsyncServiceBrowser
|
|
71
|
-
listener: BonjourListener
|
|
72
39
|
|
|
40
|
+
# ---------------- Dataclasses ----------------
|
|
73
41
|
|
|
74
|
-
def query_bonjour(service_names: list[str], ip: str) -> BonjourQuery:
|
|
75
|
-
aiozc = AsyncZeroconf(interfaces=[ip])
|
|
76
|
-
listener = BonjourListener(ip)
|
|
77
|
-
service_browser = AsyncServiceBrowser(aiozc.zeroconf, service_names,
|
|
78
|
-
handlers=[listener.async_on_service_state_change])
|
|
79
|
-
return BonjourQuery(aiozc, service_browser, listener)
|
|
80
42
|
|
|
43
|
+
# --- Dataclass decorator shim (adds slots only on 3.10+)
|
|
44
|
+
def dataclass_compat(*d_args, **d_kwargs):
|
|
45
|
+
if sys.version_info < (3, 10):
|
|
46
|
+
d_kwargs.pop("slots", None) # ignore on 3.9
|
|
47
|
+
return dataclass(*d_args, **d_kwargs)
|
|
81
48
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
49
|
+
|
|
50
|
+
@dataclass_compat(slots=True)
|
|
51
|
+
class Address:
|
|
52
|
+
ip: str
|
|
53
|
+
iface: Optional[str] # local interface name (e.g., "en0"), or None if unknown
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def full_ip(self) -> str:
|
|
57
|
+
if self.iface and self.ip.lower().startswith("fe80:"):
|
|
58
|
+
return f"{self.ip}%{self.iface}"
|
|
59
|
+
return self.ip
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass_compat(slots=True)
|
|
63
|
+
class ServiceInstance:
|
|
64
|
+
instance: str # "<Instance Name>._type._proto.local."
|
|
65
|
+
host: Optional[str] # "host.local" (without trailing dot), or None if unresolved
|
|
66
|
+
port: Optional[int] # SRV port
|
|
67
|
+
addresses: list[Address] = field(default_factory=list) # IPs with interface names
|
|
68
|
+
properties: dict[str, str] = field(default_factory=dict) # TXT key/values
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------- DNS helpers ----------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def encode_name(name: str) -> bytes:
|
|
75
|
+
name = name.rstrip(".")
|
|
76
|
+
out = bytearray()
|
|
77
|
+
for label in name.split(".") if name else []:
|
|
78
|
+
b = label.encode("utf-8")
|
|
79
|
+
if len(b) > 63:
|
|
80
|
+
raise ValueError("label too long")
|
|
81
|
+
out.append(len(b))
|
|
82
|
+
out += b
|
|
83
|
+
out.append(0)
|
|
84
|
+
return bytes(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def decode_name(data: bytes, off: int) -> tuple[str, int]:
|
|
88
|
+
labels = []
|
|
89
|
+
jumped = False
|
|
90
|
+
orig_end = off
|
|
91
|
+
for _ in range(128): # loop guard
|
|
92
|
+
if off >= len(data):
|
|
93
|
+
break
|
|
94
|
+
length = data[off]
|
|
95
|
+
if length == 0:
|
|
96
|
+
off += 1
|
|
97
|
+
break
|
|
98
|
+
if (length & 0xC0) == 0xC0:
|
|
99
|
+
if off + 1 >= len(data):
|
|
100
|
+
raise ValueError("truncated name pointer")
|
|
101
|
+
ptr = ((length & 0x3F) << 8) | data[off + 1]
|
|
102
|
+
if ptr >= len(data):
|
|
103
|
+
raise ValueError("bad name pointer")
|
|
104
|
+
if not jumped:
|
|
105
|
+
orig_end = off + 2
|
|
106
|
+
off = ptr
|
|
107
|
+
jumped = True
|
|
116
108
|
continue
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
off += 1
|
|
110
|
+
end = off + length
|
|
111
|
+
if end > len(data):
|
|
112
|
+
raise ValueError("truncated label")
|
|
113
|
+
labels.append(data[off:end].decode("utf-8", errors="replace"))
|
|
114
|
+
off = end
|
|
115
|
+
return ".".join(labels) + ".", (orig_end if jumped else off)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_query(name: str, qtype: int, unicast: bool = False) -> bytes:
|
|
119
|
+
hdr = struct.pack("!HHHHHH", 0, 0, 1, 0, 0, 0) # TXID=0, flags=0, 1 question
|
|
120
|
+
qclass = CLASS_IN | (CLASS_QU if unicast else 0)
|
|
121
|
+
return hdr + encode_name(name) + struct.pack("!HH", qtype, qclass)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_rr(data: bytes, off: int):
|
|
125
|
+
name, off = decode_name(data, off)
|
|
126
|
+
if off + 10 > len(data):
|
|
127
|
+
raise ValueError("truncated RR header")
|
|
128
|
+
rtype, rclass, ttl, rdlen = struct.unpack("!HHIH", data[off : off + 10])
|
|
129
|
+
off += 10
|
|
130
|
+
rdata = data[off : off + rdlen]
|
|
131
|
+
off += rdlen
|
|
132
|
+
|
|
133
|
+
rr = {"name": name, "type": rtype, "class": rclass & 0x7FFF, "ttl": ttl}
|
|
134
|
+
if rtype == QTYPE_PTR:
|
|
135
|
+
target, _ = decode_name(data, off - rdlen)
|
|
136
|
+
rr["ptrdname"] = target
|
|
137
|
+
elif rtype == QTYPE_SRV and rdlen >= 6:
|
|
138
|
+
priority, weight, port = struct.unpack("!HHH", rdata[:6])
|
|
139
|
+
target, _ = decode_name(data, off - rdlen + 6)
|
|
140
|
+
rr.update({"priority": priority, "weight": weight, "port": port, "target": target})
|
|
141
|
+
elif rtype == QTYPE_TXT:
|
|
142
|
+
kv = {}
|
|
143
|
+
i = 0
|
|
144
|
+
while i < rdlen:
|
|
145
|
+
b = rdata[i]
|
|
146
|
+
i += 1
|
|
147
|
+
seg = rdata[i : i + b]
|
|
148
|
+
i += b
|
|
149
|
+
if not seg:
|
|
121
150
|
continue
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
if b"=" in seg:
|
|
152
|
+
k, v = seg.split(b"=", 1)
|
|
153
|
+
kv[k.decode()] = v.decode(errors="replace")
|
|
154
|
+
else:
|
|
155
|
+
kv[seg.decode()] = ""
|
|
156
|
+
rr["txt"] = kv
|
|
157
|
+
elif rtype == QTYPE_A and rdlen == 4:
|
|
158
|
+
rr["address"] = socket.inet_ntop(socket.AF_INET, rdata)
|
|
159
|
+
elif rtype == QTYPE_AAAA and rdlen == 16:
|
|
160
|
+
rr["address"] = socket.inet_ntop(socket.AF_INET6, rdata)
|
|
161
|
+
else:
|
|
162
|
+
rr["raw"] = rdata
|
|
163
|
+
return rr, off
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def parse_mdns_message(data: bytes):
|
|
167
|
+
if len(data) < 12:
|
|
168
|
+
return []
|
|
169
|
+
_, _, qd, an, ns, ar = struct.unpack("!HHHHHH", data[:12])
|
|
170
|
+
off = 12
|
|
171
|
+
for _ in range(qd):
|
|
172
|
+
_, off = decode_name(data, off)
|
|
173
|
+
off += 4
|
|
174
|
+
rrs = []
|
|
175
|
+
for _ in range(an + ns + ar):
|
|
176
|
+
rr, off = parse_rr(data, off)
|
|
177
|
+
rrs.append(rr)
|
|
178
|
+
return rrs
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------- Interface mapping helpers ----------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class _Adapters:
|
|
185
|
+
def __init__(self):
|
|
186
|
+
self.adapters = ifaddr.get_adapters() if ifaddr is not None else []
|
|
187
|
+
|
|
188
|
+
def pick_iface_for_ip(self, ip_str: str, family: int, v6_scopeid: Optional[int]) -> Optional[str]:
|
|
189
|
+
# Prefer scope id for IPv6 link-local
|
|
190
|
+
if family == socket.AF_INET6 and ip_str.lower().startswith("fe80:") and v6_scopeid:
|
|
191
|
+
try:
|
|
192
|
+
return socket.if_indextoname(v6_scopeid)
|
|
193
|
+
except OSError:
|
|
194
|
+
pass
|
|
124
195
|
|
|
196
|
+
# Otherwise, try to match destination ip to local subnet via ifaddr
|
|
197
|
+
if not self.adapters:
|
|
198
|
+
return None
|
|
199
|
+
ip = ipaddress.ip_address(ip_str)
|
|
200
|
+
best = (None, -1) # (name, prefix_len)
|
|
201
|
+
for ad in self.adapters:
|
|
202
|
+
for ipn in ad.ips:
|
|
203
|
+
if isinstance(ipn.ip, str):
|
|
204
|
+
# IPv4
|
|
205
|
+
fam = socket.AF_INET
|
|
206
|
+
ipn_ip = ipn.ip
|
|
207
|
+
else:
|
|
208
|
+
# IPv6
|
|
209
|
+
fam = socket.AF_INET6
|
|
210
|
+
ipn_ip = ipn.ip[0]
|
|
211
|
+
if fam != family:
|
|
212
|
+
continue
|
|
213
|
+
net = ipaddress.ip_network(f"{ipn_ip}/{ipn.network_prefix}", strict=False)
|
|
214
|
+
if ip in net and ipn.network_prefix > best[1]:
|
|
215
|
+
best = (ad.nice_name or ad.name, ipn.network_prefix)
|
|
216
|
+
return best[0]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------- async sockets ----------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class _DatagramProtocol(asyncio.DatagramProtocol):
|
|
223
|
+
def __init__(self, queue: asyncio.Queue):
|
|
224
|
+
self.queue = queue
|
|
225
|
+
|
|
226
|
+
def datagram_received(self, data, addr):
|
|
227
|
+
# addr: IPv4 -> (host, port); IPv6 -> (host, port, flowinfo, scopeid)
|
|
228
|
+
self.queue.put_nowait((data, addr))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def _bind_ipv4(queue: asyncio.Queue):
|
|
232
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
233
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
234
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
235
|
+
with contextlib.suppress(OSError):
|
|
236
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
237
|
+
s.bind(("0.0.0.0", MDNS_PORT))
|
|
238
|
+
try:
|
|
239
|
+
mreq = struct.pack("=4s4s", socket.inet_aton(MDNS_MCAST_V4), socket.inet_aton("0.0.0.0"))
|
|
240
|
+
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
241
|
+
except OSError:
|
|
242
|
+
pass
|
|
243
|
+
transport, _ = await asyncio.get_running_loop().create_datagram_endpoint(lambda: _DatagramProtocol(queue), sock=s)
|
|
244
|
+
return transport, s
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _bind_ipv6_all_ifaces(queue: asyncio.Queue):
|
|
248
|
+
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
|
249
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
250
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
251
|
+
with contextlib.suppress(OSError):
|
|
252
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
253
|
+
s.bind(("::", MDNS_PORT))
|
|
254
|
+
grp = socket.inet_pton(socket.AF_INET6, MDNS_MCAST_V6)
|
|
255
|
+
for ifindex, _ in socket.if_nameindex():
|
|
256
|
+
mreq6 = grp + struct.pack("@I", ifindex)
|
|
257
|
+
try:
|
|
258
|
+
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq6)
|
|
259
|
+
except OSError:
|
|
260
|
+
continue
|
|
261
|
+
transport, _ = await asyncio.get_running_loop().create_datagram_endpoint(lambda: _DatagramProtocol(queue), sock=s)
|
|
262
|
+
return transport, s
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def _open_mdns_sockets():
|
|
266
|
+
queue = asyncio.Queue()
|
|
267
|
+
transports: list[tuple[asyncio.BaseTransport, socket.socket]] = []
|
|
268
|
+
t4, s4 = await _bind_ipv4(queue)
|
|
269
|
+
transports.append((t4, s4))
|
|
270
|
+
t6, s6 = await _bind_ipv6_all_ifaces(queue)
|
|
271
|
+
transports.append((t6, s6))
|
|
272
|
+
if not transports:
|
|
273
|
+
raise RuntimeError("Failed to open mDNS sockets (UDP/5353)")
|
|
274
|
+
return transports, queue
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def _send_query_all(transports, pkt: bytes):
|
|
278
|
+
for transport, sock in transports:
|
|
279
|
+
if sock.family == socket.AF_INET:
|
|
280
|
+
transport.sendto(pkt, (MDNS_MCAST_V4, MDNS_PORT))
|
|
281
|
+
else:
|
|
282
|
+
# Send once per iface index for better reachability
|
|
283
|
+
for ifindex, _ in socket.if_nameindex():
|
|
284
|
+
transport.sendto(pkt, (MDNS_MCAST_V6, MDNS_PORT, 0, ifindex))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------- Public API ----------------
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def browse_service(service_type: str, timeout: float = 4.0) -> list[ServiceInstance]:
|
|
291
|
+
"""
|
|
292
|
+
Discover a DNS-SD/mDNS service type (e.g. "_remoted._tcp.local.") on the local network.
|
|
293
|
+
|
|
294
|
+
Returns: List[ServiceInstance] with Address(ip, iface) entries.
|
|
295
|
+
"""
|
|
296
|
+
if not service_type.endswith("."):
|
|
297
|
+
service_type += "."
|
|
298
|
+
|
|
299
|
+
transports, queue = await _open_mdns_sockets()
|
|
300
|
+
adapters = _Adapters()
|
|
301
|
+
|
|
302
|
+
ptr_targets: set[str] = set()
|
|
303
|
+
srv_map: dict[str, dict] = {}
|
|
304
|
+
txt_map: dict[str, dict] = {}
|
|
305
|
+
# host -> list[(ip, iface)]
|
|
306
|
+
host_addrs: dict[str, list[Address]] = defaultdict(list)
|
|
307
|
+
|
|
308
|
+
def _record_addr(rr_name: str, ip_str: str, pkt_addr):
|
|
309
|
+
# Determine family and possible scopeid from the packet that delivered this RR
|
|
310
|
+
family = socket.AF_INET6 if ":" in ip_str else socket.AF_INET
|
|
311
|
+
scopeid = None
|
|
312
|
+
if isinstance(pkt_addr, tuple) and len(pkt_addr) == 4: # IPv6 remote tuple
|
|
313
|
+
scopeid = pkt_addr[3]
|
|
314
|
+
iface = adapters.pick_iface_for_ip(ip_str, family, scopeid)
|
|
315
|
+
# avoid duplicates for the same host/ip
|
|
316
|
+
existing = host_addrs[rr_name]
|
|
317
|
+
if not any(a.ip == ip_str for a in existing):
|
|
318
|
+
existing.append(Address(ip=ip_str, iface=iface))
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
await _send_query_all(transports, build_query(service_type, QTYPE_PTR, unicast=False))
|
|
322
|
+
loop = asyncio.get_running_loop()
|
|
323
|
+
end = loop.time() + timeout
|
|
324
|
+
while loop.time() < end:
|
|
325
|
+
try:
|
|
326
|
+
data, pkt_addr = await asyncio.wait_for(queue.get(), timeout=end - loop.time())
|
|
327
|
+
except asyncio.TimeoutError:
|
|
328
|
+
break
|
|
329
|
+
for rr in parse_mdns_message(data):
|
|
330
|
+
t = rr.get("type")
|
|
331
|
+
if t == QTYPE_PTR and rr.get("name") == service_type:
|
|
332
|
+
ptr_targets.add(rr.get("ptrdname"))
|
|
333
|
+
elif t == QTYPE_SRV:
|
|
334
|
+
srv_map[rr["name"]] = {
|
|
335
|
+
"target": rr.get("target"),
|
|
336
|
+
"port": rr.get("port"),
|
|
337
|
+
}
|
|
338
|
+
elif t == QTYPE_TXT:
|
|
339
|
+
txt_map[rr["name"]] = rr.get("txt", {})
|
|
340
|
+
elif (t == QTYPE_A and rr.get("address")) or (t == QTYPE_AAAA and rr.get("address")):
|
|
341
|
+
_record_addr(rr["name"], rr["address"], pkt_addr)
|
|
342
|
+
finally:
|
|
343
|
+
for transport, _ in transports:
|
|
344
|
+
transport.close()
|
|
125
345
|
|
|
126
|
-
|
|
127
|
-
|
|
346
|
+
# Assemble dataclasses
|
|
347
|
+
results: list[ServiceInstance] = []
|
|
348
|
+
for inst in sorted(ptr_targets):
|
|
349
|
+
srv = srv_map.get(inst, {})
|
|
350
|
+
target = srv.get("target")
|
|
351
|
+
host = (target[:-1] if target and target.endswith(".") else target) or None
|
|
352
|
+
addrs = host_addrs.get(target, []) if target else []
|
|
353
|
+
props = txt_map.get(inst, {})
|
|
354
|
+
results.append(
|
|
355
|
+
ServiceInstance(
|
|
356
|
+
instance=inst,
|
|
357
|
+
host=host,
|
|
358
|
+
port=srv.get("port"),
|
|
359
|
+
addresses=addrs,
|
|
360
|
+
properties=props,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
return results
|
|
128
364
|
|
|
129
365
|
|
|
130
|
-
async def browse_remoted(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[
|
|
131
|
-
return await
|
|
366
|
+
async def browse_remoted(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
|
|
367
|
+
return await browse_service(REMOTED_SERVICE_NAME, timeout=timeout)
|
|
132
368
|
|
|
133
369
|
|
|
134
|
-
async def browse_mobdev2(timeout: float = DEFAULT_BONJOUR_TIMEOUT
|
|
135
|
-
|
|
136
|
-
ips = get_ipv4_addresses() + OSUTILS.get_ipv6_ips()
|
|
137
|
-
return await browse(MOBDEV2_SERVICE_NAMES, ips, timeout=timeout)
|
|
370
|
+
async def browse_mobdev2(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
|
|
371
|
+
return await browse_service(MOBDEV2_SERVICE_NAME, timeout=timeout)
|
|
138
372
|
|
|
139
373
|
|
|
140
|
-
async def browse_remotepairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[
|
|
141
|
-
return await
|
|
374
|
+
async def browse_remotepairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
|
|
375
|
+
return await browse_service(REMOTEPAIRING_SERVICE_NAME, timeout=timeout)
|
|
142
376
|
|
|
143
377
|
|
|
144
|
-
async def browse_remotepairing_manual_pairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[
|
|
145
|
-
return await
|
|
378
|
+
async def browse_remotepairing_manual_pairing(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[ServiceInstance]:
|
|
379
|
+
return await browse_service(REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAME, timeout=timeout)
|
pymobiledevice3/ca.py
CHANGED
|
@@ -23,10 +23,7 @@ def select_hash_algorithm(device_version: Union[tuple[int, int, int], str, None]
|
|
|
23
23
|
"""
|
|
24
24
|
if device_version is None:
|
|
25
25
|
return hashes.SHA256()
|
|
26
|
-
if isinstance(device_version, str)
|
|
27
|
-
parts = tuple(int(x) for x in device_version.split("."))
|
|
28
|
-
else:
|
|
29
|
-
parts = device_version
|
|
26
|
+
parts = tuple(int(x) for x in device_version.split(".")) if isinstance(device_version, str) else device_version
|
|
30
27
|
return hashes.SHA1() if parts < (4, 0, 0) else hashes.SHA256()
|
|
31
28
|
|
|
32
29
|
|
|
@@ -65,6 +62,7 @@ def serialize_private_key_pkcs8_pem(key: RSAPrivateKey) -> bytes:
|
|
|
65
62
|
# Certificate builders (empty DN, v3, KU)
|
|
66
63
|
# =======================================
|
|
67
64
|
|
|
65
|
+
|
|
68
66
|
def build_root_certificate(root_key: RSAPrivateKey, alg: hashes.HashAlgorithm) -> Certificate:
|
|
69
67
|
"""
|
|
70
68
|
Build a self-signed root (CA) certificate:
|
|
@@ -93,10 +91,10 @@ def build_root_certificate(root_key: RSAPrivateKey, alg: hashes.HashAlgorithm) -
|
|
|
93
91
|
|
|
94
92
|
|
|
95
93
|
def build_host_certificate(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
host_key: RSAPrivateKey,
|
|
95
|
+
root_cert: Certificate,
|
|
96
|
+
root_key: RSAPrivateKey,
|
|
97
|
+
alg: hashes.HashAlgorithm,
|
|
100
98
|
) -> Certificate:
|
|
101
99
|
"""
|
|
102
100
|
Build the host (leaf) certificate signed by the root:
|
|
@@ -127,9 +125,13 @@ def build_host_certificate(
|
|
|
127
125
|
x509.KeyUsage(
|
|
128
126
|
digital_signature=True,
|
|
129
127
|
key_encipherment=True,
|
|
130
|
-
key_cert_sign=False,
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
key_cert_sign=False,
|
|
129
|
+
crl_sign=False,
|
|
130
|
+
content_commitment=False,
|
|
131
|
+
data_encipherment=False,
|
|
132
|
+
key_agreement=False,
|
|
133
|
+
encipher_only=False,
|
|
134
|
+
decipher_only=False,
|
|
133
135
|
),
|
|
134
136
|
critical=True,
|
|
135
137
|
)
|
|
@@ -138,10 +140,10 @@ def build_host_certificate(
|
|
|
138
140
|
|
|
139
141
|
|
|
140
142
|
def build_device_certificate(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
device_public_key: RSAPublicKey,
|
|
144
|
+
root_cert: Certificate,
|
|
145
|
+
root_key: RSAPrivateKey,
|
|
146
|
+
alg: hashes.HashAlgorithm,
|
|
145
147
|
) -> Certificate:
|
|
146
148
|
"""
|
|
147
149
|
Build the device certificate (leaf) signed by the root:
|
|
@@ -173,9 +175,13 @@ def build_device_certificate(
|
|
|
173
175
|
x509.KeyUsage(
|
|
174
176
|
digital_signature=True,
|
|
175
177
|
key_encipherment=True,
|
|
176
|
-
key_cert_sign=False,
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
key_cert_sign=False,
|
|
179
|
+
crl_sign=False,
|
|
180
|
+
content_commitment=False,
|
|
181
|
+
data_encipherment=False,
|
|
182
|
+
key_agreement=False,
|
|
183
|
+
encipher_only=False,
|
|
184
|
+
decipher_only=False,
|
|
179
185
|
),
|
|
180
186
|
critical=True,
|
|
181
187
|
)
|
|
@@ -188,10 +194,11 @@ def build_device_certificate(
|
|
|
188
194
|
# Public API for your pairing flow (renamed)
|
|
189
195
|
# ==========================================
|
|
190
196
|
|
|
197
|
+
|
|
191
198
|
def generate_pairing_cert_chain(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
device_public_key_pem: bytes,
|
|
200
|
+
private_key: Optional[RSAPrivateKey] = None,
|
|
201
|
+
device_version: Union[tuple[int, int, int], str, None] = (4, 0, 0),
|
|
195
202
|
) -> tuple[bytes, bytes, bytes, bytes, bytes]:
|
|
196
203
|
"""
|
|
197
204
|
Generate a root→host certificate chain and a device certificate that mirror the
|
|
@@ -215,7 +222,7 @@ def generate_pairing_cert_chain(
|
|
|
215
222
|
# Device leaf (public key provided by the device)
|
|
216
223
|
dev_pub = load_pem_public_key(device_public_key_pem)
|
|
217
224
|
if not isinstance(dev_pub, RSAPublicKey):
|
|
218
|
-
raise
|
|
225
|
+
raise TypeError("device_public_key_pem must be an RSA PUBLIC KEY in PEM format")
|
|
219
226
|
device_cert = build_device_certificate(dev_pub, root_cert, root_key, alg)
|
|
220
227
|
|
|
221
228
|
return (
|
|
@@ -277,6 +284,7 @@ def create_keybag_file(file: Path, common_name: str) -> None:
|
|
|
277
284
|
private_key.private_bytes(
|
|
278
285
|
encoding=serialization.Encoding.PEM,
|
|
279
286
|
format=PrivateFormat.TraditionalOpenSSL,
|
|
280
|
-
encryption_algorithm=serialization.NoEncryption()
|
|
281
|
-
)
|
|
287
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
288
|
+
)
|
|
289
|
+
+ cer.public_bytes(encoding=serialization.Encoding.PEM)
|
|
282
290
|
)
|
|
@@ -12,20 +12,20 @@ def cli() -> None:
|
|
|
12
12
|
|
|
13
13
|
@cli.group()
|
|
14
14
|
def activation() -> None:
|
|
15
|
-
"""
|
|
15
|
+
"""Perform iCloud activation/deactivation or query the current state"""
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@activation.command(cls=Command)
|
|
20
20
|
def state(service_provider: LockdownClient):
|
|
21
|
-
"""
|
|
21
|
+
"""Get current activation state"""
|
|
22
22
|
print(MobileActivationService(service_provider).state)
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@activation.command(cls=Command)
|
|
26
|
-
@click.option(
|
|
26
|
+
@click.option("--now", is_flag=True, help="do not wait for next nonce cycle")
|
|
27
27
|
def activate(service_provider: LockdownClient, now):
|
|
28
|
-
"""
|
|
28
|
+
"""Activate device"""
|
|
29
29
|
activation_service = MobileActivationService(service_provider)
|
|
30
30
|
if not now:
|
|
31
31
|
activation_service.wait_for_activation_session()
|
|
@@ -34,11 +34,11 @@ def activate(service_provider: LockdownClient, now):
|
|
|
34
34
|
|
|
35
35
|
@activation.command(cls=Command)
|
|
36
36
|
def deactivate(service_provider: LockdownClient):
|
|
37
|
-
"""
|
|
37
|
+
"""Deactivate device"""
|
|
38
38
|
MobileActivationService(service_provider).deactivate()
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@activation.command(cls=Command)
|
|
42
42
|
def itunes(service_provider: LockdownClient):
|
|
43
|
-
"""
|
|
44
|
-
service_provider.set_value(True, key=
|
|
43
|
+
"""Tell the device that it has been connected to iTunes (useful for < iOS 4)"""
|
|
44
|
+
service_provider.set_value(True, key="iTunesHasConnected")
|