Skip to content

Commit ed98111

Browse files
committed
cli,remote,service_connection: Implement userspace wrapper connect
1 parent 972c29f commit ed98111

File tree

4 files changed

+51
-9
lines changed

4 files changed

+51
-9
lines changed

pymobiledevice3/cli/cli_common.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def wrap_callback_calling(**kwargs: dict) -> None:
149149
lockdown_service_provider = kwargs.pop('lockdown_service_provider', None)
150150
rsd_service_provider_manually = kwargs.pop('rsd_service_provider_manually', None)
151151
rsd_service_provider_using_tunneld = kwargs.pop('rsd_service_provider_using_tunneld', None)
152+
del kwargs['rsd_use_userspace_wrapper']
152153
if lockdown_service_provider is not None:
153154
service_provider = lockdown_service_provider
154155
if rsd_service_provider_manually is not None:
@@ -224,23 +225,36 @@ def udid(self, ctx, param: str, value: str) -> Optional[LockdownClient]:
224225
class RSDCommand(BaseServiceProviderCommand):
225226
def __init__(self, *args, **kwargs):
226227
super().__init__(*args, **kwargs)
228+
self.userspace_address = None
227229
self.params[:0] = [
230+
RSDOption(('rsd_use_userspace_wrapper', '--userspace-wrapper'), type=(str, int),
231+
callback=self.userspace,
232+
mutually_exclusive=['rsd_service_provider_using_tunneld'],
233+
help='\b\nhostname and port number of userspace tunnel wrapper '
234+
'(as provided by a `start-tunnel` subcommand).'),
228235
RSDOption(('rsd_service_provider_manually', '--rsd'), type=(str, int), callback=self.rsd,
229236
mutually_exclusive=['rsd_service_provider_using_tunneld'],
230237
help='\b\n'
231238
'RSD hostname and port number (as provided by a `start-tunnel` subcommand).'),
232239
RSDOption(('rsd_service_provider_using_tunneld', '--tunnel'), callback=self.tunneld,
233-
mutually_exclusive=['rsd_service_provider_manually'], envvar=TUNNEL_ENV_VAR,
240+
mutually_exclusive=[
241+
'rsd_service_provider_manually',
242+
'rsd_use_userspace_wrapper'
243+
],
244+
envvar=TUNNEL_ENV_VAR,
234245
help='\b\n'
235246
'Either an empty string to force tunneld device selection, or a UDID of a tunneld '
236247
'discovered device.\n'
237248
'The string may be suffixed with :PORT in case tunneld is not serving at the default port.\n'
238249
f'This option may also be transferred as an environment variable: {TUNNEL_ENV_VAR}')
239250
]
240251

252+
def userspace(self, ctx, param: str, value: Optional[tuple[str, int]]) -> None:
253+
self.userspace_address = value
254+
241255
def rsd(self, ctx, param: str, value: Optional[tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
242256
if value is not None:
243-
rsd = RemoteServiceDiscoveryService(value)
257+
rsd = RemoteServiceDiscoveryService(value, userspace_address=self.userspace_address)
244258
asyncio.run(rsd.connect(), debug=True)
245259
self.service_provider = rsd
246260
return self.service_provider

pymobiledevice3/remote/remote_service_discovery.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ class RSDDevice:
2828

2929

3030
class RemoteServiceDiscoveryService(LockdownServiceProvider):
31-
def __init__(self, address: tuple[str, int], name: Optional[str] = None) -> None:
31+
def __init__(
32+
self,
33+
address: tuple[str, int],
34+
userspace_address: "tuple[str, int] | None" = None,
35+
name: Optional[str] = None
36+
) -> None:
3237
super().__init__()
3338
self.name = name
34-
self.service = RemoteXPCConnection(address)
39+
self.service = RemoteXPCConnection(address, userspace_address)
40+
self.userspace_address = userspace_address
3541
self.peer_info: Optional[dict] = None
3642
self.lockdown: Optional[LockdownClient] = None
3743
self.all_values: Optional[dict] = None
@@ -75,7 +81,11 @@ def get_value(self, domain: Optional[str] = None, key: Optional[str] = None) ->
7581
return self.lockdown.get_value(domain, key)
7682

7783
def start_lockdown_service_without_checkin(self, name: str) -> ServiceConnection:
78-
return ServiceConnection.create_using_tcp(self.service.address[0], self.get_service_port(name))
84+
return ServiceConnection.create_using_tcp(
85+
self.service.address[0],
86+
self.get_service_port(name),
87+
userspace_address=self.userspace_address
88+
)
7989

8090
def start_lockdown_service(self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
8191
service = self.start_lockdown_service_without_checkin(name)
@@ -112,7 +122,10 @@ def start_lockdown_developer_service(self, name, include_escrow_bag: bool = Fals
112122
raise
113123

114124
def start_remote_service(self, name: str) -> RemoteXPCConnection:
115-
service = RemoteXPCConnection((self.service.address[0], self.get_service_port(name)))
125+
service = RemoteXPCConnection(
126+
(self.service.address[0], self.get_service_port(name)),
127+
self.userspace_address
128+
)
116129
return service
117130

118131
def start_service(self, name: str) -> Union[RemoteXPCConnection, ServiceConnection]:

pymobiledevice3/remote/remotexpc.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import socket
23
import sys
34
from asyncio import IncompleteReadError
45
from typing import AsyncIterable, Optional
@@ -37,9 +38,10 @@
3738

3839

3940
class RemoteXPCConnection:
40-
def __init__(self, address: tuple[str, int]):
41+
def __init__(self, address: tuple[str, int], userspace_address: "tuple[str, int] | None" = None):
4142
self._previous_frame_data = b''
4243
self.address = address
44+
self.userspace_address = userspace_address
4345
self.next_message_id: dict[int, int] = {ROOT_CHANNEL: 0, REPLY_CHANNEL: 0}
4446
self.peer_info = None
4547
self._reader: Optional[asyncio.StreamReader] = None
@@ -53,7 +55,15 @@ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
5355
await self.close()
5456

5557
async def connect(self) -> None:
56-
self._reader, self._writer = await asyncio.open_connection(self.address[0], self.address[1])
58+
if self.userspace_address:
59+
self._reader, self._writer = await asyncio.open_connection(*self.userspace_address)
60+
self._writer.write(
61+
socket.inet_pton(socket.AF_INET6, self.address[0])
62+
+ self.address[1].to_bytes(4, "little")
63+
)
64+
await self._writer.drain()
65+
else:
66+
self._reader, self._writer = await asyncio.open_connection(self.address[0], self.address[1])
5767
try:
5868
await self._do_handshake()
5969
except Exception: # noqa: E722

pymobiledevice3/service_connection.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def __init__(self, sock: socket.socket, mux_device: MuxDevice = None):
116116

117117
@staticmethod
118118
def create_using_tcp(hostname: str, port: int, keep_alive: bool = True,
119+
userspace_address: "None | tuple[str, int]" = None,
119120
create_connection_timeout: int = DEFAULT_TIMEOUT) -> 'ServiceConnection':
120121
"""
121122
Create a ServiceConnection using a TCP connection.
@@ -126,7 +127,11 @@ def create_using_tcp(hostname: str, port: int, keep_alive: bool = True,
126127
:param create_connection_timeout: The timeout for creating the connection.
127128
:return: A ServiceConnection object.
128129
"""
129-
sock = socket.create_connection((hostname, port), timeout=create_connection_timeout)
130+
if userspace_address:
131+
sock = socket.create_connection(userspace_address, timeout=create_connection_timeout)
132+
sock.sendall(socket.inet_pton(socket.AF_INET6, hostname) + port.to_bytes(4, "little"))
133+
else:
134+
sock = socket.create_connection((hostname, port), timeout=create_connection_timeout)
130135
sock.settimeout(None)
131136
if keep_alive:
132137
OSUTIL.set_keepalive(sock)

0 commit comments

Comments
 (0)