Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
doronz88 committed Jul 17, 2023
1 parent bdcfb09 commit e78aebc
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 48 deletions.
18 changes: 15 additions & 3 deletions pymobiledevice3/cli/cli_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import os
import uuid
from typing import List, Optional
from typing import List, Optional, Tuple

import click
import coloredlogs
Expand All @@ -14,6 +14,7 @@

from pymobiledevice3.exceptions import NoDeviceSelectedError
from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
from pymobiledevice3.usbmux import select_devices_by_connection_type


Expand Down Expand Up @@ -69,18 +70,29 @@ class Command(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.params[:0] = [
click.Option(('lockdown', '--rsd'), type=(str, int), callback=self.rsd,
help='RSD hostname and port number'),
click.Option(('lockdown', '--udid'), envvar=UDID_ENV_VAR, callback=self.udid,
help=f'Device unique identifier. You may pass {UDID_ENV_VAR} environment variable to pass this'
f' option as well'),
click.Option(('verbosity', '-v', '--verbose'), count=True, callback=set_verbosity, expose_value=False),
]
self.service_provider = None

@staticmethod
def udid(ctx, param: str, value: str) -> Optional[LockdownClient]:
def rsd(self, ctx, param: str, value: Optional[Tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
if value is not None:
with RemoteServiceDiscoveryService(value) as rsd:
self.service_provider = rsd
return self.service_provider

def udid(self, ctx, param: str, value: str) -> Optional[LockdownClient]:
if '_PYMOBILEDEVICE3_COMPLETE' in os.environ:
# prevent lockdown connection establishment when in autocomplete mode
return

if self.service_provider is not None:
return self.service_provider

if value is not None:
return create_using_usbmux(serial=value)

Expand Down
85 changes: 80 additions & 5 deletions pymobiledevice3/cli/remote.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import asyncio
import logging
import os
from typing import Optional
from typing import Optional, Tuple

import click
from cryptography.hazmat.primitives.asymmetric import rsa

from pymobiledevice3.cli.cli_common import UDID_ENV_VAR, print_json, prompt_device_list, set_verbosity
from pymobiledevice3.exceptions import NoDeviceConnectedError
from pymobiledevice3.lockdown import create_using_remote
from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService, \
get_remoted_device, get_remoted_devices
from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type

logger = logging.getLogger(__name__)


class RemoteCommand(click.Command):
class RemoteHostnameCommand(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.params[:0] = [
Expand Down Expand Up @@ -43,6 +45,25 @@ def udid(ctx, param: str, value: str) -> Optional[str]:
return prompt_device_list(device_options).hostname


class RemoteTunCommand(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.params[:0] = [
click.Option(('rsd', '--rsd'), callback=self.rsd, type=(str, int), required=True,
help='Target device hostname and port number for RSD connection'),
click.Option(('verbosity', '-v', '--verbose'), count=True, callback=set_verbosity, expose_value=False),
]

@staticmethod
def rsd(ctx, param: str, value: Tuple[str, int]) -> Optional[RemoteServiceDiscoveryService]:
if '_PYMOBILEDEVICE3_COMPLETE' in os.environ:
# prevent lockdown connection establishment when in autocomplete mode
return

with RemoteServiceDiscoveryService(value) as rsd:
return rsd


@click.group()
def cli():
""" remote cli """
Expand All @@ -55,15 +76,15 @@ def remote_cli():
pass


@remote_cli.command('rsd-info', cls=RemoteCommand)
@remote_cli.command('rsd-info', cls=RemoteHostnameCommand)
@click.option('--color/--no-color', default=True)
def rsd_info(hostname: str, color: bool):
""" show info extracted from RSD peer """
with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd:
print_json(rsd.peer_info, colored=color)


@remote_cli.command('create-listener', cls=RemoteCommand)
@remote_cli.command('create-listener', cls=RemoteHostnameCommand)
@click.option('-p', '--protocol', type=click.Choice(['quic', 'udp']))
@click.option('--color/--no-color', default=True)
def create_listener(hostname: str, protocol: str, color: bool):
Expand All @@ -74,7 +95,7 @@ def create_listener(hostname: str, protocol: str, color: bool):
print_json(service.create_listener(private_key, protocol=protocol), colored=color)


@remote_cli.command('start-quic-tunnel', cls=RemoteCommand)
@remote_cli.command('start-quic-tunnel', cls=RemoteHostnameCommand)
@click.option('--color/--no-color', default=True)
def start_quic_tunnel(hostname: str, color: bool):
""" start quic tunnel """
Expand All @@ -83,3 +104,57 @@ def start_quic_tunnel(hostname: str, color: bool):
with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd:
with create_core_device_tunnel_service(rsd, autopair=True) as service:
print_json(asyncio.run(service.start_quic_tunnel(private_key)), colored=color)


@remote_cli.group()
def tun():
pass


@tun.command('rsd-info', cls=RemoteTunCommand)
@click.option('--color/--no-color', default=True)
def tun_rsd_info(rsd: RemoteServiceDiscoveryService, color: bool):
""" show info extracted from RSD peer """
print_json(rsd.peer_info, colored=color)


@tun.command('test', cls=RemoteTunCommand)
@click.option('--color/--no-color', default=True)
def test(rsd: RemoteServiceDiscoveryService, color: bool):
""" start quic tunnel """
core_device = rsd.connect_to_service('com.apple.mobile.storage_mounter_proxy.bridge')

response = core_device.send_receive_request({
'XPCRequestDictionary': {'Command': 'CopyDevices',
'HostProcessName': 'CoreDeviceService'}})
print(response)


@tun.command('test2', cls=RemoteTunCommand)
@click.option('--color/--no-color', default=True)
def test2(rsd: RemoteServiceDiscoveryService, color: bool):
""" start quic tunnel """
core_device = rsd.connect_to_service('com.apple.syslog_relay.shim.remote')
print(core_device)
core_device.start_service('com.apple.syslog_relay')
print(core_device)


@tun.command('list-processes', cls=RemoteTunCommand)
@click.option('--color/--no-color', default=True)
def list_processes(rsd: RemoteServiceDiscoveryService, color: bool):
""" start quic tunnel """
core_device = rsd.connect_to_service('com.apple.coredevice.appservice')
response = core_device.send_receive_request({
'CoreDevice.CoreDeviceDDIProtocolVersion': XpcInt64Type(0),
'CoreDevice.action': {},
'CoreDevice.coreDeviceVersion': {
'components': [XpcUInt64Type(325), XpcUInt64Type(3), XpcUInt64Type(0),
XpcUInt64Type(0), XpcUInt64Type(0)],
'originalComponentsCount': XpcInt64Type(2),
'stringValue': '325.3'},
'CoreDevice.deviceIdentifier': '7454ABFD-F789-4F99-9EE1-5FB8F7035ECE',
'CoreDevice.featureIdentifier': 'com.apple.coredevice.feature.listprocesses',
'CoreDevice.input': {},
'CoreDevice.invocationIdentifier': '94A17AB8-0576-4E73-94C6-C0282A4F66E3'})
print(response)
21 changes: 6 additions & 15 deletions pymobiledevice3/lockdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from pymobiledevice3.pair_records import create_pairing_records_cache_folder, generate_host_id, \
get_preferred_pair_record
from pymobiledevice3.service_connection import ServiceConnection
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
from pymobiledevice3.usbmux import PlistMuxConnection
from pymobiledevice3.utils import sanitize_ios_version

Expand Down Expand Up @@ -100,7 +101,7 @@ def _inner_reconnect_on_remote_close(*args, **kwargs):
return _inner_reconnect_on_remote_close


class LockdownClient(ABC):
class LockdownClient(ABC, LockdownServiceProvider):
def __init__(self, service: ServiceConnection, host_id: str, identifier: str = None, label: str = DEFAULT_LABEL,
system_buid: str = SYSTEM_BUID, pair_record: Mapping = None, pairing_records_cache_folder: Path = None,
port: int = SERVICE_PORT):
Expand Down Expand Up @@ -430,8 +431,8 @@ def set_value(self, value, domain: str = None, key: str = None) -> Mapping:
return self._request('SetValue', options)

def get_service_connection_attributes(self, name, escrow_bag=None) -> Mapping:
if not self.paired:
raise NotPairedError()
# if not self.paired:
# raise NotPairedError()

options = {'Service': name}
if escrow_bag is not None:
Expand Down Expand Up @@ -464,16 +465,6 @@ async def aio_start_service(self, name: str, escrow_bag=None) -> ServiceConnecti
await service_connection.aio_ssl_start(f)
return service_connection

def start_developer_service(self, name, escrow_bag=None) -> ServiceConnection:
try:
return self.start_service(name, escrow_bag)
except StartServiceError:
self.logger.error(
'Failed to connect to required service. Make sure DeveloperDiskImage.dmg has been mounted. '
'You can do so using: pymobiledevice3 mounter mount'
)
raise

def close(self) -> None:
self.service.close()

Expand Down Expand Up @@ -690,8 +681,8 @@ def create_using_remote(hostname: str, identifier: str = None, label: str = DEFA
service.send_plist({'Label': label, 'ProtocolVersion': '2', 'Request': 'RSDCheckin'})

# we expect two responses after the first request
service.recv_plist()
service.recv_plist()
print(service.recv_plist())
print(service.recv_plist())

client = TcpLockdownClient.create(
service, identifier=identifier, label=label, local_hostname=local_hostname, pair_record=pair_record,
Expand Down
24 changes: 24 additions & 0 deletions pymobiledevice3/lockdown_service_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import logging
from abc import abstractmethod

from pymobiledevice3.exceptions import StartServiceError
from pymobiledevice3.service_connection import ServiceConnection


class LockdownServiceProvider:
@abstractmethod
def start_service(self, name: str, escrow_bag=None, **args) -> ServiceConnection:
pass

async def aio_start_service(self, name: str, escrow_bag=None, **args) -> ServiceConnection:
pass

def start_developer_service(self, name, escrow_bag=None, **args) -> ServiceConnection:
try:
return self.start_service(name, escrow_bag, **args)
except StartServiceError:
logging.getLogger(self.__module__).error(
'Failed to connect to required service. Make sure DeveloperDiskImage.dmg has been mounted. '
'You can do so using: pymobiledevice3 mounter mount'
)
raise
40 changes: 34 additions & 6 deletions pymobiledevice3/remote/remote_service_discovery.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from dataclasses import dataclass
from typing import List, Tuple
from typing import List, Tuple, Union

from pymobiledevice3.exceptions import NoDeviceConnectedError
from pymobiledevice3.exceptions import NoDeviceConnectedError, PyMobileDevice3Exception
from pymobiledevice3.remote.bonjour import DEFAULT_BONJOUR_TIMEOUT, get_remoted_addresses
from pymobiledevice3.remote.remotexpc import RemoteXPCConnection
from pymobiledevice3.service_connection import ServiceConnection
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider


@dataclass
Expand All @@ -18,7 +20,8 @@ class RSDDevice:
RSD_PORT = 58783


class RemoteServiceDiscoveryService:
class RemoteServiceDiscoveryService(LockdownServiceProvider):

def __init__(self, address: Tuple[str, int]):
self.service = RemoteXPCConnection(address)
self.peer_info = None
Expand All @@ -27,9 +30,34 @@ def connect(self) -> None:
self.service.connect()
self.peer_info = self.service.receive_response()

def connect_to_service(self, name: str) -> RemoteXPCConnection:
service_port = int(self.peer_info['Services'][name]['Port'])
service = RemoteXPCConnection((self.service.address[0], service_port))
def start_service(self, name: str, escrow_bag: bytes = None,
requires_rsd_checkin: bool = True) -> ServiceConnection:
return self.connect_to_service(name, escrow_bag=escrow_bag, requires_rsd_checkin=requires_rsd_checkin)

def connect_to_service(self, name: str, escrow_bag: bytes = None, requires_rsd_checkin: bool = True) -> \
Union[RemoteXPCConnection, ServiceConnection]:
service = self.peer_info['Services'][name]
service_port = int(service['Port'])
service_properties = service.get('Properties', {})
use_remote_xpc = service_properties.get('UsesRemoteXPC', False)
service_version = service_properties.get('ServiceVersion')
if not use_remote_xpc:
service = ServiceConnection.create_using_tcp(self.service.address[0], service_port)
if requires_rsd_checkin:
checkin = {'Label': 'pymobiledevice3', 'ProtocolVersion': '2', 'Request': 'RSDCheckin'}
if escrow_bag is not None:
checkin['EscrowBag'] = escrow_bag
response = service.send_recv_plist(checkin)
if response['Request'] != 'RSDCheckin':
raise PyMobileDevice3Exception(
f'Invalid response for RSDCheckIn: {response}. Expected "RSDCheckIn"')
response = service.recv_plist()
if response['Request'] != 'StartService':
raise PyMobileDevice3Exception(
f'Invalid response for RSDCheckIn: {response}. Expected "ServiceService"')
return service

service = RemoteXPCConnection((self.service.address[0], service_port), service_version)
service.connect()
return service

Expand Down
3 changes: 2 additions & 1 deletion pymobiledevice3/remote/remotexpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@


class RemoteXPCConnection:
def __init__(self, address: Tuple[str, int]):
def __init__(self, address: Tuple[str, int], service_version: int = 2):
self.address = address
self.service_version = service_version
self.sock: Optional[socket.socket] = None
self.next_message_id = 0
self.peer_info = None
Expand Down
5 changes: 4 additions & 1 deletion pymobiledevice3/remote/xpc_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def _decode_xpc_object(xpc_object) -> Any:

def get_object_from_xpc_wrapper(payload: bytes):
payload = XpcWrapper.parse(payload).message.payload
# print(payload)
if payload is None:
return None
return _decode_xpc_object(payload.obj)
Expand Down Expand Up @@ -252,10 +253,12 @@ def _build_xpc_object(payload: Any) -> Mapping:
return builder(payload)


def create_xpc_wrapper(d: Mapping, message_id: int = 0) -> bytes:
def create_xpc_wrapper(d: Mapping, message_id: int = 0, service_version=2) -> bytes:
flags = XpcFlags.ALWAYS_SET
if len(d.keys()) > 0:
flags |= XpcFlags.DATA_PRESENT
if service_version == 1:
flags |= XpcFlags.HEARTBEAT_REQUEST

xpc_payload = {
'message_id': message_id,
Expand Down
10 changes: 5 additions & 5 deletions pymobiledevice3/services/base_service.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import logging

from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.service_connection import ServiceConnection
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider


class BaseService:
def __init__(self, lockdown: LockdownClient, service_name: str, is_developer_service=False,
service: ServiceConnection = None):
def __init__(self, lockdown: LockdownServiceProvider, service_name: str, is_developer_service=False,
service: ServiceConnection = None, requires_rsd_checkin: bool = True):
"""
:param lockdown: lockdown connection
:param service_name: wrapped service name - will attempt
:param is_developer_service: should DeveloperDiskImage be mounted before
:param service: an established service connection object. If none, will attempt connecting to service_name
"""

if not service:
if service is None:
start_service = lockdown.start_developer_service if is_developer_service else lockdown.start_service
service = start_service(service_name)
service = start_service(service_name, requires_rsd_checkin=requires_rsd_checkin)

self.service_name = service_name
self.lockdown = lockdown
Expand Down
Loading

0 comments on commit e78aebc

Please sign in to comment.