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.
Files changed (143) hide show
  1. misc/plist_sniffer.py +15 -15
  2. misc/remotexpc_sniffer.py +29 -28
  3. pymobiledevice3/__main__.py +123 -98
  4. pymobiledevice3/_version.py +2 -2
  5. pymobiledevice3/bonjour.py +351 -117
  6. pymobiledevice3/ca.py +32 -24
  7. pymobiledevice3/cli/activation.py +7 -7
  8. pymobiledevice3/cli/afc.py +19 -19
  9. pymobiledevice3/cli/amfi.py +4 -4
  10. pymobiledevice3/cli/apps.py +51 -39
  11. pymobiledevice3/cli/backup.py +58 -32
  12. pymobiledevice3/cli/bonjour.py +27 -20
  13. pymobiledevice3/cli/cli_common.py +112 -81
  14. pymobiledevice3/cli/companion_proxy.py +4 -4
  15. pymobiledevice3/cli/completions.py +10 -10
  16. pymobiledevice3/cli/crash.py +37 -31
  17. pymobiledevice3/cli/developer.py +601 -519
  18. pymobiledevice3/cli/diagnostics.py +38 -33
  19. pymobiledevice3/cli/lockdown.py +82 -72
  20. pymobiledevice3/cli/mounter.py +84 -67
  21. pymobiledevice3/cli/notification.py +10 -10
  22. pymobiledevice3/cli/pcap.py +19 -14
  23. pymobiledevice3/cli/power_assertion.py +12 -10
  24. pymobiledevice3/cli/processes.py +10 -10
  25. pymobiledevice3/cli/profile.py +88 -77
  26. pymobiledevice3/cli/provision.py +17 -17
  27. pymobiledevice3/cli/remote.py +188 -111
  28. pymobiledevice3/cli/restore.py +43 -40
  29. pymobiledevice3/cli/springboard.py +30 -28
  30. pymobiledevice3/cli/syslog.py +85 -58
  31. pymobiledevice3/cli/usbmux.py +21 -20
  32. pymobiledevice3/cli/version.py +3 -2
  33. pymobiledevice3/cli/webinspector.py +156 -78
  34. pymobiledevice3/common.py +1 -1
  35. pymobiledevice3/exceptions.py +154 -60
  36. pymobiledevice3/irecv.py +49 -53
  37. pymobiledevice3/irecv_devices.py +1489 -492
  38. pymobiledevice3/lockdown.py +400 -251
  39. pymobiledevice3/lockdown_service_provider.py +5 -7
  40. pymobiledevice3/osu/os_utils.py +18 -9
  41. pymobiledevice3/osu/posix_util.py +28 -15
  42. pymobiledevice3/osu/win_util.py +14 -8
  43. pymobiledevice3/pair_records.py +19 -19
  44. pymobiledevice3/remote/common.py +4 -4
  45. pymobiledevice3/remote/core_device/app_service.py +94 -67
  46. pymobiledevice3/remote/core_device/core_device_service.py +17 -14
  47. pymobiledevice3/remote/core_device/device_info.py +5 -5
  48. pymobiledevice3/remote/core_device/diagnostics_service.py +10 -8
  49. pymobiledevice3/remote/core_device/file_service.py +47 -33
  50. pymobiledevice3/remote/remote_service_discovery.py +53 -35
  51. pymobiledevice3/remote/remotexpc.py +64 -42
  52. pymobiledevice3/remote/tunnel_service.py +383 -297
  53. pymobiledevice3/remote/utils.py +14 -13
  54. pymobiledevice3/remote/xpc_message.py +145 -125
  55. pymobiledevice3/resources/dsc_uuid_map.py +19 -19
  56. pymobiledevice3/resources/firmware_notifications.py +16 -16
  57. pymobiledevice3/restore/asr.py +27 -27
  58. pymobiledevice3/restore/base_restore.py +90 -47
  59. pymobiledevice3/restore/consts.py +87 -66
  60. pymobiledevice3/restore/device.py +11 -11
  61. pymobiledevice3/restore/fdr.py +46 -46
  62. pymobiledevice3/restore/ftab.py +19 -19
  63. pymobiledevice3/restore/img4.py +130 -133
  64. pymobiledevice3/restore/mbn.py +587 -0
  65. pymobiledevice3/restore/recovery.py +125 -135
  66. pymobiledevice3/restore/restore.py +535 -523
  67. pymobiledevice3/restore/restore_options.py +122 -115
  68. pymobiledevice3/restore/restored_client.py +25 -22
  69. pymobiledevice3/restore/tss.py +378 -270
  70. pymobiledevice3/service_connection.py +50 -46
  71. pymobiledevice3/services/accessibilityaudit.py +137 -127
  72. pymobiledevice3/services/afc.py +363 -293
  73. pymobiledevice3/services/amfi.py +21 -18
  74. pymobiledevice3/services/companion.py +23 -19
  75. pymobiledevice3/services/crash_reports.py +61 -47
  76. pymobiledevice3/services/debugserver_applist.py +3 -3
  77. pymobiledevice3/services/device_arbitration.py +8 -8
  78. pymobiledevice3/services/device_link.py +56 -48
  79. pymobiledevice3/services/diagnostics.py +971 -968
  80. pymobiledevice3/services/dtfetchsymbols.py +8 -8
  81. pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
  82. pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
  83. pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
  84. pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
  85. pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
  86. pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
  87. pymobiledevice3/services/dvt/instruments/device_info.py +11 -11
  88. pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
  89. pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
  90. pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
  91. pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
  92. pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
  93. pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
  94. pymobiledevice3/services/dvt/instruments/process_control.py +25 -10
  95. pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
  96. pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
  97. pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
  98. pymobiledevice3/services/file_relay.py +10 -10
  99. pymobiledevice3/services/heartbeat.py +8 -7
  100. pymobiledevice3/services/house_arrest.py +12 -15
  101. pymobiledevice3/services/installation_proxy.py +119 -100
  102. pymobiledevice3/services/lockdown_service.py +12 -5
  103. pymobiledevice3/services/misagent.py +22 -19
  104. pymobiledevice3/services/mobile_activation.py +84 -72
  105. pymobiledevice3/services/mobile_config.py +331 -301
  106. pymobiledevice3/services/mobile_image_mounter.py +137 -116
  107. pymobiledevice3/services/mobilebackup2.py +188 -150
  108. pymobiledevice3/services/notification_proxy.py +11 -11
  109. pymobiledevice3/services/os_trace.py +128 -74
  110. pymobiledevice3/services/pcapd.py +306 -306
  111. pymobiledevice3/services/power_assertion.py +10 -9
  112. pymobiledevice3/services/preboard.py +4 -4
  113. pymobiledevice3/services/remote_fetch_symbols.py +16 -14
  114. pymobiledevice3/services/remote_server.py +176 -146
  115. pymobiledevice3/services/restore_service.py +16 -16
  116. pymobiledevice3/services/screenshot.py +13 -10
  117. pymobiledevice3/services/simulate_location.py +7 -7
  118. pymobiledevice3/services/springboard.py +15 -15
  119. pymobiledevice3/services/syslog.py +5 -5
  120. pymobiledevice3/services/web_protocol/alert.py +3 -3
  121. pymobiledevice3/services/web_protocol/automation_session.py +183 -179
  122. pymobiledevice3/services/web_protocol/cdp_screencast.py +44 -36
  123. pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
  124. pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
  125. pymobiledevice3/services/web_protocol/driver.py +47 -45
  126. pymobiledevice3/services/web_protocol/element.py +74 -63
  127. pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
  128. pymobiledevice3/services/web_protocol/selenium_api.py +3 -3
  129. pymobiledevice3/services/web_protocol/session_protocol.py +15 -10
  130. pymobiledevice3/services/web_protocol/switch_to.py +11 -12
  131. pymobiledevice3/services/webinspector.py +142 -116
  132. pymobiledevice3/tcp_forwarder.py +64 -50
  133. pymobiledevice3/tunneld/api.py +20 -15
  134. pymobiledevice3/tunneld/server.py +315 -193
  135. pymobiledevice3/usbmux.py +197 -148
  136. pymobiledevice3/utils.py +14 -11
  137. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/METADATA +2 -6
  138. pymobiledevice3-5.1.2.dist-info/RECORD +173 -0
  139. pymobiledevice3-4.27.0.dist-info/RECORD +0 -172
  140. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/WHEEL +0 -0
  141. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/entry_points.txt +0 -0
  142. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/licenses/LICENSE +0 -0
  143. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/top_level.txt +0 -0
@@ -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 dataclasses
3
- from asyncio import CancelledError
4
- from socket import AF_INET, AF_INET6, inet_ntop
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
- from ifaddr import get_adapters
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
- REMOTEPAIRING_SERVICE_NAMES = ['_remotepairing._tcp.local.']
14
- REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAMES = ['_remotepairing-manual-pairing._tcp.local.']
15
- MOBDEV2_SERVICE_NAMES = ['_apple-mobdev2._tcp.local.']
16
- REMOTED_SERVICE_NAMES = ['_remoted._tcp.local.']
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
- @dataclasses.dataclass
22
- class BonjourAnswer:
23
- name: str
24
- properties: dict[bytes, bytes]
25
- ips: list[str]
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
- async def browse(service_names: list[str], ips: list[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) \
83
- -> list[BonjourAnswer]:
84
- bonjour_queries = [query_bonjour(service_names, adapter) for adapter in ips]
85
- answers = []
86
- try:
87
- await asyncio.sleep(timeout)
88
- except CancelledError:
89
- for bonjour_query in bonjour_queries:
90
- await bonjour_query.listener.close()
91
- await bonjour_query.service_browser.async_cancel()
92
- await bonjour_query.zc.async_close()
93
- raise
94
- for bonjour_query in bonjour_queries:
95
- if bonjour_query.listener.addresses:
96
- answer = BonjourAnswer(
97
- bonjour_query.listener.name, bonjour_query.listener.properties, bonjour_query.listener.addresses,
98
- bonjour_query.listener.port)
99
- if answer not in answers:
100
- answers.append(answer)
101
- await bonjour_query.listener.close()
102
- await bonjour_query.service_browser.async_cancel()
103
- await bonjour_query.zc.async_close()
104
- return answers
105
-
106
-
107
- async def browse_ipv6(service_names: list[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
108
- return await browse(service_names, OSUTILS.get_ipv6_ips(), timeout=timeout)
109
-
110
-
111
- def get_ipv4_addresses() -> list[str]:
112
- ips = []
113
- for adapter in get_adapters():
114
- if adapter.nice_name.startswith('tun'):
115
- # skip browsing on already established tunnels
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
- for ip in adapter.ips:
118
- if ip.ip == '127.0.0.1':
119
- continue
120
- if not ip.is_IPv4:
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
- ips.append(ip.ip)
123
- return ips
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
- async def browse_ipv4(service_names: list[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[BonjourAnswer]:
127
- return await browse(service_names, get_ipv4_addresses(), timeout=timeout)
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[BonjourAnswer]:
131
- return await browse_ipv6(REMOTED_SERVICE_NAMES, timeout=timeout)
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, ips: Optional[list[str]] = None) -> list[BonjourAnswer]:
135
- if ips is None:
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[BonjourAnswer]:
141
- return await browse_ipv4(REMOTEPAIRING_SERVICE_NAMES, timeout=timeout)
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[BonjourAnswer]:
145
- return await browse_ipv4(REMOTEPAIRING_MANUAL_PAIRING_SERVICE_NAMES, timeout=timeout)
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
- host_key: RSAPrivateKey,
97
- root_cert: Certificate,
98
- root_key: RSAPrivateKey,
99
- alg: hashes.HashAlgorithm,
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, crl_sign=False,
131
- content_commitment=False, data_encipherment=False,
132
- key_agreement=False, encipher_only=False, decipher_only=False,
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
- device_public_key: RSAPublicKey,
142
- root_cert: Certificate,
143
- root_key: RSAPrivateKey,
144
- alg: hashes.HashAlgorithm,
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, crl_sign=False,
177
- content_commitment=False, data_encipherment=False,
178
- key_agreement=False, encipher_only=False, decipher_only=False,
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
- device_public_key_pem: bytes,
193
- private_key: Optional[RSAPrivateKey] = None,
194
- device_version: Union[tuple[int, int, int], str, None] = (4, 0, 0),
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 ValueError("device_public_key_pem must be an RSA PUBLIC KEY in PEM format")
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
- ) + cer.public_bytes(encoding=serialization.Encoding.PEM)
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
- """ Perform iCloud activation/deactivation or query the current state """
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
- """ Get current activation state """
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('--now', is_flag=True, help='do not wait for next nonce cycle')
26
+ @click.option("--now", is_flag=True, help="do not wait for next nonce cycle")
27
27
  def activate(service_provider: LockdownClient, now):
28
- """ Activate device """
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
- """ Deactivate device """
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
- """ Tell the device that it has been connected to iTunes (useful for < iOS 4) """
44
- service_provider.set_value(True, key='iTunesHasConnected')
43
+ """Tell the device that it has been connected to iTunes (useful for < iOS 4)"""
44
+ service_provider.set_value(True, key="iTunesHasConnected")