From 345dee6e099cb94f70c4a554cc0a463c02a46d37 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Thu, 9 Feb 2023 18:42:35 +0200 Subject: [PATCH 001/234] requirements: pycrashreport>=1.0.5 (#403) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 35992a4fa..07833cbc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ cmd2 packaging pygnuutils>=0.0.6 cryptography>=35.0.0 -pycrashreport>=1.0.2 +pycrashreport>=1.0.5 fastapi[all] uvicorn>=0.15.0 starlette From 0e18b6bb30c2a92c5440e3fb15784594f1011f04 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Thu, 9 Feb 2023 18:51:27 +0200 Subject: [PATCH 002/234] setup: bump version to 1.36.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 01361a9a1..21f21e99e 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.36.5' +VERSION = '1.36.6' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 8bcd1cc70530f847fb387a78378181fe0b2fe4ad Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 10 Feb 2023 17:49:10 +0200 Subject: [PATCH 003/234] device_info: fix flake8 ambiguous variable name --- pymobiledevice3/services/dvt/instruments/device_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/services/dvt/instruments/device_info.py b/pymobiledevice3/services/dvt/instruments/device_info.py index 9d9bc9875..082a192ea 100644 --- a/pymobiledevice3/services/dvt/instruments/device_info.py +++ b/pymobiledevice3/services/dvt/instruments/device_info.py @@ -65,7 +65,7 @@ def kpep_database(self) -> typing.Mapping: def trace_codes(self): codes_file = self.request_information('traceCodesFile') - return {int(k, 16): v for k, v in map(lambda l: l.split(), codes_file.splitlines())} + return {int(k, 16): v for k, v in map(lambda line: line.split(), codes_file.splitlines())} def request_information(self, selector_name): self._channel[selector_name]() From 7d50c3d25b24ea0526c59739877d7bdfc1c8890b Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 10 Feb 2023 17:49:58 +0200 Subject: [PATCH 004/234] python-app: fail on E741 (ambigous name) --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d21a0ed09..e59c06af7 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,7 +26,7 @@ jobs: run: | python -m pip install flake8 # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82,F401 --show-source --statistics + flake8 . --count --select=E9,F63,F7,F82,F401,E741 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Verify sorted imports From f2962a8f43f7ed2ece039c919464e8fbc3831e7b Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 10 Feb 2023 17:50:39 +0200 Subject: [PATCH 005/234] accessibilityaudit: fix notification handling (#405) --- .../services/accessibilityaudit.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/pymobiledevice3/services/accessibilityaudit.py b/pymobiledevice3/services/accessibilityaudit.py index 6c1d7b083..a281356b3 100644 --- a/pymobiledevice3/services/accessibilityaudit.py +++ b/pymobiledevice3/services/accessibilityaudit.py @@ -101,16 +101,9 @@ def __init__(self, lockdown: LockdownClient): super().__init__(lockdown, self.SERVICE_NAME, remove_ssl_context=True) self.broadcast.deviceSetAppMonitoringEnabled_(MessageAux().append_obj(True)) - self.recv_response() - self.broadcast.deviceInspectorSetMonitoredEventType_(MessageAux().append_obj(0)) - self.recv_response() - self.broadcast.deviceInspectorShowVisuals_(MessageAux().append_obj(1)) - self.recv_response() - self.broadcast.deviceInspectorShowIgnoredElements_(MessageAux().append_obj(1)) - self.recv_response() def iter_notifications(self): while True: @@ -120,24 +113,29 @@ def iter_notifications(self): data = [x['value'] for x in notification[1]] yield notification[0], deserialize_object(data) - def recv_response(self): - plist = self.recv_plist() - - while plist[1] is not None: - # skip notifications, but report them if exists - yield plist[0], deserialize_object(plist[1]) + def recv_response(self) -> typing.Generator[typing.Tuple, None, None]: + """ + Responses are tuples in the one of the following forms: + - (None, None) + - (eventName, eventData) + - (eventData, None) + """ + while True: plist = self.recv_plist() + yield plist[0], deserialize_object(plist[1]) - return plist - - def device_capabilities(self): + def device_capabilities(self) -> typing.List[str]: self.broadcast.deviceCapabilities() - return self.recv_response()[0] + for response in self.recv_response(): + if isinstance(response[0], list): + return response[0] - def get_current_settings(self): + def get_current_settings(self) -> typing.List[AXAuditDeviceSetting_v1]: self.broadcast.deviceAccessibilitySettings() - return deserialize_object(self.recv_response()[0]) + for response in self.recv_response(): + if isinstance(response[0], dict): + return deserialize_object(response[0]) def move_focus_next(self): self.move_focus(DIRECTION_NEXT) From b7de14d15bc9b2dfa180e0312db9a15f2ecd5490 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 10 Feb 2023 18:01:58 +0200 Subject: [PATCH 006/234] setup: bump version to 1.36.7 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 21f21e99e..bdc872989 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.36.6' +VERSION = '1.36.7' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 91984c3d2cb321a510a5c333825647825457f26d Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sat, 11 Feb 2023 21:00:54 +0200 Subject: [PATCH 007/234] accessibility: refactor --- pymobiledevice3/cli/developer.py | 34 +++-- .../services/accessibilityaudit.py | 123 +++++++++++------- tests/services/test_accessibility.py | 22 ++++ 3 files changed, 111 insertions(+), 68 deletions(-) create mode 100644 tests/services/test_accessibility.py diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 71f17ae4d..9048f5ad7 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -5,6 +5,7 @@ import posixpath import shlex import signal +import time from collections import namedtuple from dataclasses import asdict from datetime import datetime @@ -722,7 +723,7 @@ def accessibility(): @accessibility.command('capabilities', cls=Command) def accessibility_capabilities(lockdown: LockdownClient): """ display accessibility capabilities """ - print_json(AccessibilityAudit(lockdown).device_capabilities()) + print_json(AccessibilityAudit(lockdown).capabilities) @accessibility.group('settings') @@ -734,7 +735,7 @@ def accessibility_settings(): @accessibility_settings.command('show', cls=Command) def accessibility_settings_show(lockdown: LockdownClient): """ show current settings """ - for setting in AccessibilityAudit(lockdown).get_current_settings(): + for setting in AccessibilityAudit(lockdown).settings: print(setting) @@ -756,39 +757,36 @@ def accessibility_shell(lockdown: LockdownClient): @accessibility.command('notifications', cls=Command) -@click.option('-c', '--cycle-focus', is_flag=True) -def accessibility_notifications(lockdown: LockdownClient, cycle_focus): +def accessibility_notifications(lockdown: LockdownClient): """ show notifications """ service = AccessibilityAudit(lockdown) - if cycle_focus: - service.move_focus_next() - for name, data in service.iter_notifications(): - if name in ('hostAppStateChanged:', - 'hostInspectorCurrentElementChanged:',): - for focus_item in data: + for event in service.iter_events(): + if event.name in ('hostAppStateChanged:', + 'hostInspectorCurrentElementChanged:',): + for focus_item in event.data: logger.info(focus_item) - if name == 'hostInspectorCurrentElementChanged:': - if cycle_focus: - service.move_focus_next() - @accessibility.command('list-items', cls=Command) def accessibility_list_items(lockdown: LockdownClient): """ list items available in currently shown menu """ service = AccessibilityAudit(lockdown) - iterator = service.iter_notifications() + iterator = service.iter_events() + + # every focus change is expected publish a "hostInspectorCurrentElementChanged:" service.move_focus_next() first_item = None - for name, data in iterator: - if name != 'hostInspectorCurrentElementChanged:': + for event in iterator: + if event.name != 'hostInspectorCurrentElementChanged:': + # ignore any other events continue - current_item = data[0] + # each such event should contain exactly one element that became in focus + current_item = event.data[0] if first_item is None: first_item = current_item diff --git a/pymobiledevice3/services/accessibilityaudit.py b/pymobiledevice3/services/accessibilityaudit.py index a281356b3..5cd0fc563 100644 --- a/pymobiledevice3/services/accessibilityaudit.py +++ b/pymobiledevice3/services/accessibilityaudit.py @@ -1,4 +1,6 @@ import typing +from dataclasses import dataclass +from enum import Enum from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.remote_server import MessageAux, RemoteServer @@ -56,9 +58,16 @@ def __init__(self, fields): if k not in self._fields: self._fields[k] = None - def __str__(self): - return f'' + @property + def key(self) -> str: + return self._fields['IdentiifierValue_v1'] + + @property + def value(self) -> typing.Any: + return self._fields['CurrentValueNumber_v1'] + + def __str__(self) -> str: + return f'' SERIALIZABLE_OBJECTS = { @@ -69,10 +78,18 @@ def __str__(self): 'AXAuditElementAttribute_v1': AXAuditElementAttribute_v1, } -DIRECTION_PREV = 3 -DIRECTION_NEXT = 4 -DIRECTION_FIRST = 5 -DIRECTION_LAST = 6 + +@dataclass +class Event: + name: str + data: SerializedObject + + +class Direction(Enum): + Previous = 3 + Next = 4 + First = 5 + Last = 6 def deserialize_object(d): @@ -100,47 +117,55 @@ class AccessibilityAudit(RemoteServer): def __init__(self, lockdown: LockdownClient): super().__init__(lockdown, self.SERVICE_NAME, remove_ssl_context=True) - self.broadcast.deviceSetAppMonitoringEnabled_(MessageAux().append_obj(True)) - self.broadcast.deviceInspectorSetMonitoredEventType_(MessageAux().append_obj(0)) - self.broadcast.deviceInspectorShowVisuals_(MessageAux().append_obj(1)) - self.broadcast.deviceInspectorShowIgnoredElements_(MessageAux().append_obj(1)) + # flush previously received messages + self.recv_plist() + self.recv_plist() - def iter_notifications(self): - while True: - notification = self.recv_plist() - if notification[1] is None: - continue - data = [x['value'] for x in notification[1]] - yield notification[0], deserialize_object(data) + @property + def capabilities(self) -> typing.List[str]: + self.broadcast.deviceCapabilities() + return self.recv_plist()[0] - def recv_response(self) -> typing.Generator[typing.Tuple, None, None]: - """ - Responses are tuples in the one of the following forms: + @property + def settings(self) -> typing.List[AXAuditDeviceSetting_v1]: + self.broadcast.deviceAccessibilitySettings() + return deserialize_object(self.recv_plist()[0]) - - (None, None) - - (eventName, eventData) - - (eventData, None) - """ - while True: - plist = self.recv_plist() - yield plist[0], deserialize_object(plist[1]) + def perform_handshake(self) -> None: + # this service acts differently from others, requiring no handshake + pass - def device_capabilities(self) -> typing.List[str]: - self.broadcast.deviceCapabilities() - for response in self.recv_response(): - if isinstance(response[0], list): - return response[0] + def set_app_monitoring_enabled(self, value: bool) -> None: + self.broadcast.deviceSetAppMonitoringEnabled_(MessageAux().append_obj(value), expects_reply=False) - def get_current_settings(self) -> typing.List[AXAuditDeviceSetting_v1]: - self.broadcast.deviceAccessibilitySettings() - for response in self.recv_response(): - if isinstance(response[0], dict): - return deserialize_object(response[0]) + def set_monitored_event_type(self, event_type: int = None) -> None: + if event_type is None: + event_type = 0 + self.broadcast.deviceInspectorSetMonitoredEventType_(MessageAux().append_obj(event_type), expects_reply=False) + + def set_show_ignored_elements(self, value: bool) -> None: + self.broadcast.deviceInspectorShowIgnoredElements_(MessageAux().append_obj(int(value)), expects_reply=False) + + def set_show_visuals(self, value: bool) -> None: + self.broadcast.deviceInspectorShowVisuals_(MessageAux().append_obj(int(value)), expects_reply=False) + + def iter_events(self, app_monitoring_enabled=True, monitored_event_type: int = None) -> \ + typing.Generator[Event, None, None]: + + self.set_app_monitoring_enabled(app_monitoring_enabled) + self.set_monitored_event_type(monitored_event_type) + + while True: + message = self.recv_plist() + if message[1] is None: + continue + data = [x['value'] for x in message[1]] + yield Event(name=message[0], data=deserialize_object(data)) - def move_focus_next(self): - self.move_focus(DIRECTION_NEXT) + def move_focus_next(self) -> None: + self.move_focus(Direction.Next) - def perform_press(self, element: bytes): + def perform_press(self, element: bytes) -> None: """ simulate click (can be used only for processes with task_for_pid-allow """ element = { 'ObjectType': 'AXAuditElement_v1', @@ -193,10 +218,9 @@ def perform_press(self, element: bytes): } self.broadcast.deviceElement_performAction_withValue_( - MessageAux().append_obj(element).append_obj(action).append_obj(0)) - self.recv_response() + MessageAux().append_obj(element).append_obj(action).append_obj(0), expects_reply=False) - def move_focus(self, direction): + def move_focus(self, direction: Direction) -> None: options = { 'ObjectType': 'passthrough', 'Value': { @@ -206,7 +230,7 @@ def move_focus(self, direction): }, 'direction': { 'ObjectType': 'passthrough', - 'Value': direction, + 'Value': direction.value, }, 'includeContainers': { 'ObjectType': 'passthrough', @@ -215,10 +239,9 @@ def move_focus(self, direction): } } - self.broadcast.deviceInspectorMoveWithOptions_(MessageAux().append_obj(options)) - self.recv_response() + self.broadcast.deviceInspectorMoveWithOptions_(MessageAux().append_obj(options), expects_reply=False) - def set_setting(self, name, value): + def set_setting(self, name: str, value: typing.Any) -> None: setting = {'ObjectType': 'AXAuditDeviceSetting_v1', 'Value': {'ObjectType': 'passthrough', 'Value': {'CurrentValueNumber_v1': {'ObjectType': 'passthrough', @@ -229,5 +252,5 @@ def set_setting(self, name, value): 'SettingTypeValue_v1': {'ObjectType': 'passthrough', 'Value': 3}, 'SliderTickMarksValue_v1': {'ObjectType': 'passthrough', 'Value': 0}}}} self.broadcast.deviceUpdateAccessibilitySetting_withValue_( - MessageAux().append_obj(setting).append_obj({'ObjectType': 'passthrough', 'Value': value})) - self.recv_response() + MessageAux().append_obj(setting).append_obj({'ObjectType': 'passthrough', 'Value': value}), + expects_reply=False) diff --git a/tests/services/test_accessibility.py b/tests/services/test_accessibility.py new file mode 100644 index 000000000..221113e5b --- /dev/null +++ b/tests/services/test_accessibility.py @@ -0,0 +1,22 @@ +import pytest + +from pymobiledevice3.services.accessibilityaudit import AccessibilityAudit + + +@pytest.fixture(scope='function') +def accessibility_audit(lockdown): + with AccessibilityAudit(lockdown=lockdown) as accessibility_audit: + yield accessibility_audit + + +def test_capabilities(accessibility_audit): + assert 'deviceApiVersion' in accessibility_audit.capabilities + + +def test_invert_colors_in_settings(accessibility_audit): + found = False + for setting in accessibility_audit.settings: + if setting.key == 'INVERT_COLORS': + found = True + break + assert found From fe1126be88b8f01b8741ec9a7155f788b8789729 Mon Sep 17 00:00:00 2001 From: matan Date: Wed, 15 Feb 2023 14:47:32 +0200 Subject: [PATCH 008/234] accessibility: Fix audit for iOS < 15.0 --- pymobiledevice3/services/accessibilityaudit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/services/accessibilityaudit.py b/pymobiledevice3/services/accessibilityaudit.py index 5cd0fc563..5435f8eb2 100644 --- a/pymobiledevice3/services/accessibilityaudit.py +++ b/pymobiledevice3/services/accessibilityaudit.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from enum import Enum +from packaging.version import Version + from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.remote_server import MessageAux, RemoteServer @@ -119,7 +121,8 @@ def __init__(self, lockdown: LockdownClient): # flush previously received messages self.recv_plist() - self.recv_plist() + if Version(lockdown.product_version) >= Version('15.0'): + self.recv_plist() @property def capabilities(self) -> typing.List[str]: From b935ab30da9748af1fd7e77d4cb9e2902b368df6 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 16 Feb 2023 09:24:25 +0200 Subject: [PATCH 009/234] setup: bump version to 1.37.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bdc872989..8d15ed00c 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.36.7' +VERSION = '1.37.0' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 17e9efedcf678e007165b7759976acc0e5cbc88e Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 17 Feb 2023 22:11:14 +0200 Subject: [PATCH 010/234] cli: handle `PasswordRequiredError` gracefully --- pymobiledevice3/__main__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index 5295e070f..4a4a8bf91 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -29,8 +29,8 @@ from pymobiledevice3.cli.webinspector import cli as webinspector_cli from pymobiledevice3.exceptions import DeveloperModeError, DeveloperModeIsNotEnabledError, DeviceHasPasscodeSetError, \ InternalError, InvalidServiceError, MessageNotSupportedError, MissingValueError, NoDeviceConnectedError, \ - NoDeviceSelectedError, NotPairedError, PairingDialogResponsePendingError, SetProhibitedError, \ - UsbmuxConnectionError, UserDeniedPairingError + NoDeviceSelectedError, NotPairedError, PairingDialogResponsePendingError, PasswordRequiredError, \ + SetProhibitedError, UsbmuxConnectionError, UserDeniedPairingError coloredlogs.install(level=logging.INFO) @@ -86,6 +86,8 @@ def cli(): 'You may try: python3 -m pymobiledevice3 mounter auto-mount') except NoDeviceSelectedError: return + except PasswordRequiredError: + logger.error('Device is password protected. Please unlock and retry') except BrokenPipeError: traceback.print_exc() From abdcd9b9f3000e550255125f12749cc1e79ed23b Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 17 Feb 2023 22:11:41 +0200 Subject: [PATCH 011/234] lockdown: add `device_class` property --- pymobiledevice3/lockdown.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index b3b68f736..879ec6bd2 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -9,6 +9,7 @@ import time import uuid from contextlib import contextmanager, suppress +from enum import Enum from pathlib import Path from typing import Mapping, Optional, Union @@ -95,6 +96,15 @@ def _reconnect_on_remote_close(*args, **kwargs): 'com.apple.Accessibility', ] +class DeviceClass(Enum): + iPhone = 'iPhone' + iPad = 'iPad' + iPod = 'iPod' + Watch = 'Watch' + AppleTV = 'AppleTV' + Unknown = 'Unknown' + + class LockdownClient(object): DEFAULT_CLIENT_NAME = 'pymobiledevice3' SERVICE_PORT = 62078 @@ -188,6 +198,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): def query_type(self) -> str: return self._request('QueryType').get('Type') + @property + def device_class(self) -> DeviceClass: + try: + return DeviceClass(self.all_values.get('DeviceClass')) + except ValueError: + return DeviceClass('Unknown') + @property def wifi_mac_address(self) -> str: return self.all_values.get('WiFiAddress') From 3855bc9618afb7078d8dcad6c3dff8f0c17330a5 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 17 Feb 2023 22:12:28 +0200 Subject: [PATCH 012/234] lockdown: fix `ValidatePair` request requirements (#408) --- pymobiledevice3/lockdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 879ec6bd2..de907d509 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -384,7 +384,7 @@ def validate_pairing(self) -> bool: if self.pair_record is None: return False - if Version(self.product_version) < Version('11.0'): + if (Version(self.product_version) < Version('7.0')) and (self.device_class != DeviceClass.Watch): try: self._request('ValidatePair', {'PairRecord': self.pair_record}) except PairingError: From ae9deba51fbe3a54725fae6b5be9eef839608c8b Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 17 Feb 2023 22:12:54 +0200 Subject: [PATCH 013/234] lockdown: save `EscrowBag` only when available (#408) --- pymobiledevice3/lockdown.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index de907d509..fdcc13229 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -433,7 +433,11 @@ def pair(self, timeout: int = None) -> None: pair = self._request_pair(pair_options, timeout=timeout) pair_record['HostPrivateKey'] = private_key_pem - pair_record['EscrowBag'] = pair.get('EscrowBag') + escrow_bag = pair.get('EscrowBag') + + if escrow_bag is not None: + pair_record['EscrowBag'] = pair.get('EscrowBag') + self.pair_record = pair_record self._write_storage_file(f'{self.identifier}.plist', plistlib.dumps(pair_record)) From a7564dd60813288ad80217caddd847d7115ee6ea Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sat, 18 Feb 2023 20:39:35 +0200 Subject: [PATCH 014/234] usbmux: refactor exceptions --- pymobiledevice3/__main__.py | 10 +++++----- pymobiledevice3/exceptions.py | 27 +++++++++++++++------------ pymobiledevice3/lockdown.py | 10 +++++----- pymobiledevice3/service_connection.py | 7 +------ pymobiledevice3/usbmux.py | 26 +++++++++++++++++++------- 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index 4a4a8bf91..1d556e477 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -27,10 +27,10 @@ from pymobiledevice3.cli.syslog import cli as syslog_cli from pymobiledevice3.cli.usbmux import cli as usbmux_cli from pymobiledevice3.cli.webinspector import cli as webinspector_cli -from pymobiledevice3.exceptions import DeveloperModeError, DeveloperModeIsNotEnabledError, DeviceHasPasscodeSetError, \ - InternalError, InvalidServiceError, MessageNotSupportedError, MissingValueError, NoDeviceConnectedError, \ - NoDeviceSelectedError, NotPairedError, PairingDialogResponsePendingError, PasswordRequiredError, \ - SetProhibitedError, UsbmuxConnectionError, UserDeniedPairingError +from pymobiledevice3.exceptions import ConnectionFailedError, DeveloperModeError, DeveloperModeIsNotEnabledError, \ + DeviceHasPasscodeSetError, InternalError, InvalidServiceError, MessageNotSupportedError, MissingValueError, \ + NoDeviceConnectedError, NoDeviceSelectedError, NotPairedError, PairingDialogResponsePendingError, \ + PasswordRequiredError, SetProhibitedError, UserDeniedPairingError coloredlogs.install(level=logging.INFO) @@ -72,7 +72,7 @@ def cli(): logger.error('Cannot enable developer-mode when passcode is set') except DeveloperModeError as e: logger.error(f'Failed to enable developer-mode. Error: {e}') - except UsbmuxConnectionError: + except ConnectionFailedError: logger.error('Failed to connect to usbmuxd socket. Make sure it\'s running.') except MessageNotSupportedError: logger.error('Message not supported for this iOS version') diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index a95406c66..bc41f2bbb 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -1,6 +1,6 @@ __all__ = [ 'PyMobileDevice3Exception', 'DeviceVersionNotSupportedError', 'IncorrectModeError', 'DeviceVersionFormatError', - 'ConnectionFailedError', 'NotTrustedError', 'PairingError', 'NotPairedError', 'CannotStopSessionError', + 'NotTrustedError', 'PairingError', 'NotPairedError', 'CannotStopSessionError', 'PasswordRequiredError', 'StartServiceError', 'FatalPairingError', 'NoDeviceConnectedError', 'MuxException', 'MuxVersionError', 'ArgumentError', 'AfcException', 'AfcFileNotFoundError', 'DvtException', 'DvtDirListError', 'NotMountedError', 'AlreadyMountedError', 'UnsupportedCommandError', 'ExtractingStackshotError', @@ -8,9 +8,9 @@ 'ArbitrationError', 'InternalError', 'DeveloperModeIsNotEnabledError', 'DeviceAlreadyInUseError', 'LockdownError', 'PairingDialogResponsePendingError', 'UserDeniedPairingError', 'InvalidHostIDError', 'SetProhibitedError', 'MissingValueError', 'PasscodeRequiredError', 'AmfiError', 'DeviceHasPasscodeSetError', 'NotificationTimeoutError', - 'DeveloperModeError', 'ProfileError', 'UsbmuxConnectionError', 'IRecvError', 'IRecvNoDeviceConnectedError', + 'DeveloperModeError', 'ProfileError', 'IRecvError', 'IRecvNoDeviceConnectedError', 'NoDeviceSelectedError', 'MessageNotSupportedError', 'InvalidServiceError', 'InspectorEvaluateError', - 'LaunchingApplicationError', + 'LaunchingApplicationError', 'BadCommandError', 'BadDevError', 'ConnectionFailedError', ] @@ -30,10 +30,6 @@ class DeviceVersionFormatError(PyMobileDevice3Exception): pass -class ConnectionFailedError(PyMobileDevice3Exception): - pass - - class NotTrustedError(PyMobileDevice3Exception): pass @@ -74,6 +70,18 @@ class MuxVersionError(MuxException): pass +class BadCommandError(MuxException): + pass + + +class BadDevError(MuxException): + pass + + +class ConnectionFailedError(MuxException): + pass + + class ArgumentError(PyMobileDevice3Exception): pass @@ -227,11 +235,6 @@ class ProfileError(PyMobileDevice3Exception): pass -class UsbmuxConnectionError(PyMobileDevice3Exception): - """ error connecting to usbmuxd socket """ - pass - - class IRecvError(PyMobileDevice3Exception): pass diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index fdcc13229..3c7095bb7 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -18,10 +18,10 @@ from pymobiledevice3 import usbmux from pymobiledevice3.ca import ca_do_everything from pymobiledevice3.common import get_home_folder -from pymobiledevice3.exceptions import CannotStopSessionError, ConnectionFailedError, ConnectionTerminatedError, \ - FatalPairingError, IncorrectModeError, InvalidHostIDError, InvalidServiceError, LockdownError, MissingValueError, \ - NotPairedError, PairingDialogResponsePendingError, PairingError, PasscodeRequiredError, PasswordRequiredError, \ - SetProhibitedError, StartServiceError, UserDeniedPairingError +from pymobiledevice3.exceptions import CannotStopSessionError, ConnectionTerminatedError, FatalPairingError, \ + IncorrectModeError, InvalidHostIDError, InvalidServiceError, LockdownError, MissingValueError, NotPairedError, \ + PairingDialogResponsePendingError, PairingError, PasscodeRequiredError, PasswordRequiredError, SetProhibitedError, \ + StartServiceError, UserDeniedPairingError from pymobiledevice3.irecv_devices import IRECV_DEVICES from pymobiledevice3.service_connection import Medium, ServiceConnection from pymobiledevice3.usbmux import PlistMuxConnection @@ -539,7 +539,7 @@ async def aio_start_service(self, name: str, escrow_bag=None) -> ServiceConnecti def start_developer_service(self, name, escrow_bag=None) -> ServiceConnection: try: return self.start_service(name, escrow_bag) - except (StartServiceError, ConnectionFailedError): + 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' diff --git a/pymobiledevice3/service_connection.py b/pymobiledevice3/service_connection.py index f63a294e5..c12d39879 100755 --- a/pymobiledevice3/service_connection.py +++ b/pymobiledevice3/service_connection.py @@ -11,7 +11,6 @@ import IPython from pygments import formatters, highlight, lexers -from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionTerminatedError, NoDeviceConnectedError, \ PyMobileDevice3Exception from pymobiledevice3.usbmux import MuxDevice, select_device @@ -89,11 +88,7 @@ def create_using_usbmux(udid: Optional[str], port: int, connection_type: str = N if udid: raise ConnectionFailedError() raise NoDeviceConnectedError() - try: - sock = target_device.connect(port) - except usbmux.MuxException: - raise ConnectionFailedError(f'Connection to device port {port} failed') - + sock = target_device.connect(port) return ServiceConnection(sock, mux_device=target_device) @staticmethod diff --git a/pymobiledevice3/usbmux.py b/pymobiledevice3/usbmux.py index ca88aa609..0a9b18bd2 100644 --- a/pymobiledevice3/usbmux.py +++ b/pymobiledevice3/usbmux.py @@ -9,7 +9,8 @@ from construct import Const, CString, Enum, FixedSized, GreedyBytes, Int16ul, Int32ul, Padding, Prefixed, StreamError, \ Struct, Switch, this -from pymobiledevice3.exceptions import MuxException, MuxVersionError, NotPairedError, UsbmuxConnectionError +from pymobiledevice3.exceptions import BadCommandError, BadDevError, ConnectionFailedError, MuxException, \ + MuxVersionError, NotPairedError usbmuxd_version = Enum(Int32ul, BINARY=0, @@ -143,8 +144,8 @@ def create_usbmux_socket() -> SafeStreamSocket: return SafeStreamSocket(MuxConnection.ITUNES_HOST, socket.AF_INET) else: return SafeStreamSocket(MuxConnection.USBMUXD_PIPE, socket.AF_UNIX) - except ConnectionRefusedError as e: - raise UsbmuxConnectionError from e + except ConnectionRefusedError: + raise ConnectionFailedError() @staticmethod def create(): @@ -209,6 +210,16 @@ def _assert_not_connected(self): if self._connected: raise MuxException('Mux is connected, cannot issue control packets') + def _raise_mux_exception(self, result: int, message: str = None): + exceptions = { + int(usbmuxd_result.BADCOMMAND): BadCommandError, + int(usbmuxd_result.BADDEV): BadDevError, + int(usbmuxd_result.CONNREFUSED): ConnectionFailedError, + int(usbmuxd_result.BADVERSION): MuxVersionError, + } + exception = exceptions.get(result, MuxException) + raise exception(message) + class BinaryMuxConnection(MuxConnection): """ old binary protocol """ @@ -251,8 +262,9 @@ def _connect(self, device_id: int, port: int): raise MuxException(f'unexpected message type received: {response}') if response.data.result != usbmuxd_result.OK: - raise MuxException(f'failed to connect to device: {device_id} at port: {port}. reason: ' - f'{response.data.result}') + raise self._raise_mux_exception(int(response.data.result), + f'failed to connect to device: {device_id} at port: {port}. reason: ' + f'{response.data.result}') def _send(self, data: Mapping): self._assert_not_connected() @@ -275,7 +287,7 @@ def _send_receive(self, message_type: int): result = response.data.result if result != usbmuxd_result.OK: - raise MuxException(f'{message_type} failed: error {result}') + raise self._raise_mux_exception(int(result), f'{message_type} failed: error {result}') def _add_device(self, device: MuxDevice): self.devices.append(device) @@ -360,7 +372,7 @@ def _send_receive(self, data: Mapping): if response['MessageType'] != 'Result': raise MuxException(f'got an invalid message: {response}') if response['Number'] != 0: - raise MuxException(f'got an error message: {response}') + raise self._raise_mux_exception(response['Number'], f'got an error message: {response}') def create_mux() -> MuxConnection: From b68047cba27b506a9db890e8bb2682411aa87c44 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sat, 18 Feb 2023 20:39:45 +0200 Subject: [PATCH 015/234] tcp_forwarder: allow forwarding without pairing --- pymobiledevice3/cli/developer.py | 3 ++- pymobiledevice3/cli/usbmux.py | 9 +++++---- pymobiledevice3/tcp_forwarder.py | 32 ++++++++++++++++++++++---------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 9048f5ad7..85b4e3117 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -860,7 +860,8 @@ def debugserver_start_server(lockdown: LockdownClient, local_port): (lldb) platform connect connect://localhost: """ attr = lockdown.get_service_connection_attributes('com.apple.debugserver.DVTSecureSocketProxy') - TcpForwarder(lockdown, local_port, attr['Port'], attr.get('EnableServiceSSL', False)).start() + TcpForwarder(local_port, attr['Port'], serial=lockdown.identifier, + enable_ssl=attr.get('EnableServiceSSL', False)).start() @developer.group('arbitration') diff --git a/pymobiledevice3/cli/usbmux.py b/pymobiledevice3/cli/usbmux.py index 1921d1c43..0f0d3a6b9 100644 --- a/pymobiledevice3/cli/usbmux.py +++ b/pymobiledevice3/cli/usbmux.py @@ -4,7 +4,7 @@ import click from pymobiledevice3 import usbmux -from pymobiledevice3.cli.cli_common import Command, print_json +from pymobiledevice3.cli.cli_common import print_json from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.tcp_forwarder import TcpForwarder @@ -23,13 +23,14 @@ def usbmux_cli(): pass -@usbmux_cli.command('forward', cls=Command) +@usbmux_cli.command('forward') @click.argument('src_port', type=click.IntRange(1, 0xffff)) @click.argument('dst_port', type=click.IntRange(1, 0xffff)) +@click.option('--serial', help='device serial number') @click.option('-d', '--daemonize', is_flag=True) -def usbmux_forward(lockdown: LockdownClient, src_port, dst_port, daemonize): +def usbmux_forward(src_port: int, dst_port: int, serial: str, daemonize: bool): """ forward tcp port """ - forwarder = TcpForwarder(lockdown, src_port, dst_port) + forwarder = TcpForwarder(src_port, dst_port, serial=serial) if daemonize: try: diff --git a/pymobiledevice3/tcp_forwarder.py b/pymobiledevice3/tcp_forwarder.py index 2993a7a20..d3c22d9b4 100644 --- a/pymobiledevice3/tcp_forwarder.py +++ b/pymobiledevice3/tcp_forwarder.py @@ -3,8 +3,10 @@ import socket import threading +from pymobiledevice3 import usbmux +from pymobiledevice3.exceptions import ConnectionFailedError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.service_connection import ConnectionFailedError, ServiceConnection +from pymobiledevice3.service_connection import ServiceConnection class TcpForwarder: @@ -15,19 +17,20 @@ class TcpForwarder: MAX_FORWARDED_CONNECTIONS = 200 TIMEOUT = 1 - def __init__(self, lockdown: LockdownClient, src_port: int, dst_port: int, enable_ssl=False, - listening_event: threading.Event = None): + def __init__(self, src_port: int, dst_port: int, serial: str = None, enable_ssl=False, + listening_event: threading.Event = None, usbmux_connection_type: str = None): """ Initialize a new tcp forwarder - :param lockdown: lockdown connection :param src_port: tcp port to listen on :param dst_port: tcp port to connect to each new connection via the supplied lockdown object + :param serial: device serial :param enable_ssl: enable ssl wrapping for the transferred data :param listening_event: event to fire when the listening occurred + :param usbmux_connection_type: preferred connection type """ self.logger = logging.getLogger(__name__) - self.lockdown = lockdown + self.serial = serial self.src_port = src_port self.dst_port = dst_port self.server_socket = None @@ -35,6 +38,7 @@ def __init__(self, lockdown: LockdownClient, src_port: int, dst_port: int, enabl self.enable_ssl = enable_ssl self.stopped = threading.Event() self.listening_event = listening_event + self.usbmux_connection_type = usbmux_connection_type # dictionaries containing the required maps to transfer data between each local # socket to its remote socket and vice versa @@ -109,14 +113,22 @@ def _handle_server_connection(self): local_connection.setblocking(False) try: - service_connection = ServiceConnection.create_using_usbmux( - self.lockdown.udid, self.dst_port, connection_type=self.lockdown.usbmux_connection_type) - if self.enable_ssl: - with self.lockdown.ssl_file() as ssl_file: + # use the lockdown pairing record + lockdown = LockdownClient(self.serial, usbmux_connection_type=self.usbmux_connection_type) + service_connection = ServiceConnection.create_using_usbmux( + self.serial, self.dst_port, connection_type=self.usbmux_connection_type) + + with lockdown.ssl_file() as ssl_file: service_connection.ssl_start(ssl_file) - remote_connection = service_connection.socket + remote_connection = service_connection.socket + else: + # connect directly using usbmuxd + mux_device = usbmux.select_device(self.serial, connection_type=self.usbmux_connection_type) + if mux_device is None: + raise ConnectionFailedError() + remote_connection = mux_device.connect(self.dst_port) except ConnectionFailedError: self.logger.error(f'failed to connect to port: {self.dst_port}') local_connection.close() From db47f2ea2312fb6c69f207ff0331dec6d0d6f820 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sun, 19 Feb 2023 00:04:59 +0200 Subject: [PATCH 016/234] usbmux: fix unclosed socket on bad connects --- pymobiledevice3/usbmux.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/usbmux.py b/pymobiledevice3/usbmux.py index 0a9b18bd2..60b96fb34 100644 --- a/pymobiledevice3/usbmux.py +++ b/pymobiledevice3/usbmux.py @@ -82,8 +82,13 @@ class MuxDevice: serial: str connection_type: str - def connect(self, port) -> socket.socket: - return create_mux().connect(self, port) + def connect(self, port: int) -> socket.socket: + mux = create_mux() + try: + return mux.connect(self, port) + except: # noqa: E722 + mux.close() + raise @property def is_usb(self) -> bool: From 9b10412858fece3577ada5b41664769bc9665a6f Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sun, 19 Feb 2023 00:05:23 +0200 Subject: [PATCH 017/234] tcp_forwarder: fix unclosed sockets on close --- pymobiledevice3/tcp_forwarder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pymobiledevice3/tcp_forwarder.py b/pymobiledevice3/tcp_forwarder.py index d3c22d9b4..7b5860f43 100644 --- a/pymobiledevice3/tcp_forwarder.py +++ b/pymobiledevice3/tcp_forwarder.py @@ -78,6 +78,10 @@ def start(self, address='0.0.0.0'): for current_sock in exceptional: self._handle_close_or_error(current_sock) + # on stop, close all currently opened sockets + for current_sock in self.inputs: + current_sock.close() + def _handle_close_or_error(self, from_sock): """ if an error occurred its time to close the two sockets """ other_sock = self.connections[from_sock] From 2578eaae8f01e4a80d3cc8018a154e340cf992ac Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sun, 19 Feb 2023 00:05:59 +0200 Subject: [PATCH 018/234] tests: refactor cli/test_mounter.py -> tests/test_utils.py --- tests/{cli/test_mounter.py => test_utils.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{cli/test_mounter.py => test_utils.py} (100%) diff --git a/tests/cli/test_mounter.py b/tests/test_utils.py similarity index 100% rename from tests/cli/test_mounter.py rename to tests/test_utils.py From 0f94b156564ad812619d15bb64af1ec9ae022566 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sun, 19 Feb 2023 00:06:17 +0200 Subject: [PATCH 019/234] tests: add tests for tcp forwarder --- tests/services/test_tcp_forwarder.py | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/services/test_tcp_forwarder.py diff --git a/tests/services/test_tcp_forwarder.py b/tests/services/test_tcp_forwarder.py new file mode 100644 index 000000000..637398109 --- /dev/null +++ b/tests/services/test_tcp_forwarder.py @@ -0,0 +1,34 @@ +import threading +from socket import socket + +import pytest + +from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.tcp_forwarder import TcpForwarder + +FREE_PORT = 3582 + + +def attempt_local_connection(port: int): + client = socket() + client.connect(('127.0.0.1', port)) + client.close() + + +@pytest.mark.parametrize('dst_port', [FREE_PORT, LockdownClient.SERVICE_PORT]) +def test_tcp_forwarder_bad_port(lockdown: LockdownClient, dst_port: int): + # start forwarder + listening_event = threading.Event() + forwarder = TcpForwarder(FREE_PORT, dst_port, listening_event=listening_event) + thread = threading.Thread(target=forwarder.start) + thread.start() + + # wait for it to actually start listening + listening_event.wait() + attempt_local_connection(FREE_PORT) + + # tell it to stop + forwarder.stop() + + # make sure it stops + thread.join() From 9d169d44b86974eff1876622b00ff30db90cabec Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 19 Feb 2023 08:11:57 +0200 Subject: [PATCH 020/234] lockdown: fix `DeviceClass` enum values --- pymobiledevice3/lockdown.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 3c7095bb7..a1e51473d 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -97,12 +97,12 @@ def _reconnect_on_remote_close(*args, **kwargs): class DeviceClass(Enum): - iPhone = 'iPhone' - iPad = 'iPad' - iPod = 'iPod' - Watch = 'Watch' - AppleTV = 'AppleTV' - Unknown = 'Unknown' + IPHONE = 'iPhone' + IPAD = 'iPad' + IPOD = 'iPod' + WATCH = 'Watch' + APPLE_TV = 'AppleTV' + UNKNOWN = 'Unknown' class LockdownClient(object): @@ -384,7 +384,7 @@ def validate_pairing(self) -> bool: if self.pair_record is None: return False - if (Version(self.product_version) < Version('7.0')) and (self.device_class != DeviceClass.Watch): + if (Version(self.product_version) < Version('7.0')) and (self.device_class != DeviceClass.WATCH): try: self._request('ValidatePair', {'PairRecord': self.pair_record}) except PairingError: From 5686c1cd8c56592cd10a594af71c50fd40499ceb Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 19 Feb 2023 08:38:23 +0200 Subject: [PATCH 021/234] setup: bump version to 1.38.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8d15ed00c..140d4b2a3 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.37.0' +VERSION = '1.38.0' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From a4b13a4d82eefd12a7e41e8975078e454deb2523 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 20 Feb 2023 14:51:44 +0200 Subject: [PATCH 022/234] requirements: ipsw_parser>=1.1.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 07833cbc7..5107d8287 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,5 +28,5 @@ nest_asyncio>=1.5.5 Pillow inquirer pyimg4>=0.7 -ipsw_parser>=1.1.0 +ipsw_parser>=1.1.2 remotezip From ecd4b36716837fbae72e1c31474f3ec9d6edeeca Mon Sep 17 00:00:00 2001 From: doronz88 Date: Mon, 20 Feb 2023 15:08:16 +0200 Subject: [PATCH 023/234] setup: bump version to 1.38.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 140d4b2a3..f4a7ee68e 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.38.0' +VERSION = '1.38.1' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From e6f05f6ea6431a414aa4ca91f57b0780c91a0442 Mon Sep 17 00:00:00 2001 From: Yotam Olenik Date: Sun, 5 Mar 2023 13:33:42 +0200 Subject: [PATCH 024/234] lockdown: bugfix: remove irrelevant wifi-pairing property --- pymobiledevice3/lockdown.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index a1e51473d..8d2b78863 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -20,8 +20,8 @@ from pymobiledevice3.common import get_home_folder from pymobiledevice3.exceptions import CannotStopSessionError, ConnectionTerminatedError, FatalPairingError, \ IncorrectModeError, InvalidHostIDError, InvalidServiceError, LockdownError, MissingValueError, NotPairedError, \ - PairingDialogResponsePendingError, PairingError, PasscodeRequiredError, PasswordRequiredError, SetProhibitedError, \ - StartServiceError, UserDeniedPairingError + PairingDialogResponsePendingError, PairingError, PasswordRequiredError, SetProhibitedError, StartServiceError, \ + UserDeniedPairingError from pymobiledevice3.irecv_devices import IRECV_DEVICES from pymobiledevice3.service_connection import Medium, ServiceConnection from pymobiledevice3.usbmux import PlistMuxConnection @@ -249,17 +249,6 @@ def invert_display(self) -> bool: def invert_display(self, value: bool) -> None: self.set_value(int(value), 'com.apple.Accessibility', 'InvertDisplayEnabledByiTunes') - @property - def enable_wifi_pairing(self) -> bool: - return self.get_value('com.apple.mobile.wireless_lockdown').get('EnableWifiPairing', False) - - @enable_wifi_pairing.setter - def enable_wifi_pairing(self, value: bool) -> None: - try: - self.set_value(value, 'com.apple.mobile.wireless_lockdown', 'EnableWifiPairing') - except MissingValueError as e: - raise PasscodeRequiredError from e - @property def enable_wifi_connections(self) -> bool: return self.get_value('com.apple.mobile.wireless_lockdown').get('EnableWifiConnections', False) From 4ad8c43941e9af7838107f90beacbc6839f77612 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 6 Mar 2023 14:17:05 +0200 Subject: [PATCH 025/234] setup: bump version to 1.38.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f4a7ee68e..75c7425ba 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.38.1' +VERSION = '1.38.2' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 90efe2c0d9197fcdd6425f44a566334b14cfb700 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Tue, 7 Mar 2023 09:55:12 +0200 Subject: [PATCH 026/234] cli: fix accessibility settings parameters for ios 16.4 --- pymobiledevice3/cli/developer.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 85b4e3117..73fb3e732 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -740,13 +740,16 @@ def accessibility_settings_show(lockdown: LockdownClient): @accessibility_settings.command('set', cls=Command) -@click.argument('setting', type=click.Choice( - ['INVERT_COLORS', 'INCREASE_CONTRAST', 'REDUCE_TRANSPARENCY', 'REDUCE_MOTION', 'FONT_SIZE'])) -@click.argument('value', type=click.INT) +@click.argument('setting') +@click.argument('value') def accessibility_settings_set(lockdown: LockdownClient, setting, value): - """ show current settings """ + """ + change current settings + + in order to list all available use the "show" command + """ service = AccessibilityAudit(lockdown) - service.set_setting(setting, value) + service.set_setting(setting, eval(value)) wait_return() From b36f71c24887289b4c067e9836afad79f9e1ccdf Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 9 Mar 2023 08:41:47 +0200 Subject: [PATCH 027/234] requirements: pycrashreport>=1.0.6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5107d8287..a1295483a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ cmd2 packaging pygnuutils>=0.0.6 cryptography>=35.0.0 -pycrashreport>=1.0.5 +pycrashreport>=1.0.6 fastapi[all] uvicorn>=0.15.0 starlette From e5c7d7ee6970f9ad9b234bba0dfe576b90a87055 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 9 Mar 2023 09:13:11 +0200 Subject: [PATCH 028/234] setup: bump version to 1.38.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 75c7425ba..64c02eb8a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.38.2' +VERSION = '1.38.3' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 1bc4b6945270af9bbdfd7ff1a87c633e5f60e1a2 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 9 Mar 2023 09:44:48 +0200 Subject: [PATCH 029/234] cli: change `sysmon process single` output to json --- pymobiledevice3/cli/developer.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 73fb3e732..f5da4f3f4 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -263,17 +263,15 @@ def sysmon_process_monitor(lockdown: LockdownClient, threshold): @sysmon_process.command('single', cls=Command) -@click.option('-f', '--fields', help='show only given field names splitted by ",".') @click.option('-a', '--attributes', multiple=True, help='filter processes by given attribute value given as key=value') -def sysmon_process_single(lockdown: LockdownClient, fields, attributes): +@click.option('--color/--no-color', default=True) +def sysmon_process_single(lockdown: LockdownClient, attributes: List[str], color: bool): """ show a single snapshot of currently running processes. """ - if fields is not None: - fields = fields.split(',') - count = 0 + result = [] with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: device_info = DeviceInfo(dvt) @@ -299,14 +297,11 @@ def sysmon_process_single(lockdown: LockdownClient, fields, attributes): # adding "artificially" the execName field process['execName'] = device_info.execname_for_pid(process['pid']) - - print(f'{process["name"]} ({process["pid"]})') - for name, value in process.items(): - if (fields is None) or (name in fields): - print(f'\t{name}: {value}') + result.append(process) # exit after single snapshot - return + break + print_json(result, colored=color) @sysmon.command('system', cls=Command) From 91716e615635da899bbcb9aa42a048670825c600 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 12 Mar 2023 13:49:48 +0200 Subject: [PATCH 030/234] cli: make `backup2 encryption` case insensitive --- pymobiledevice3/cli/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/backup.py b/pymobiledevice3/cli/backup.py index 629dfdfed..4dcdee8fc 100644 --- a/pymobiledevice3/cli/backup.py +++ b/pymobiledevice3/cli/backup.py @@ -126,7 +126,7 @@ def extract(lockdown: LockdownClient, domain_name, relative_path, backup_directo @backup2.command(cls=Command) -@click.argument('mode', type=click.Choice(['ON', 'OFF'])) +@click.argument('mode', type=click.Choice(['ON', 'OFF'], case_sensitive=False)) @click.argument('password') @backup_directory_option def encryption(lockdown: LockdownClient, backup_directory, mode, password): From d803e9d705101f581e1cf3ce36b7ada2e99e52a5 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Tue, 14 Mar 2023 15:33:36 +0200 Subject: [PATCH 031/234] setup: bump version to 1.39.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 64c02eb8a..61565ff80 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.38.3' +VERSION = '1.39.0' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 614973c6b466a5a4a424e0e33a9f2f23e577c174 Mon Sep 17 00:00:00 2001 From: Yotam Olenik Date: Thu, 16 Mar 2023 14:18:31 +0200 Subject: [PATCH 032/234] power_assertion: make `create_power_assertion()` into a context-manager --- pymobiledevice3/cli/power_assertion.py | 6 +++++- pymobiledevice3/services/power_assertion.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/cli/power_assertion.py b/pymobiledevice3/cli/power_assertion.py index 4349e109f..7e449925d 100644 --- a/pymobiledevice3/cli/power_assertion.py +++ b/pymobiledevice3/cli/power_assertion.py @@ -1,3 +1,5 @@ +import time + import click from pymobiledevice3.cli.cli_common import Command @@ -19,4 +21,6 @@ def cli(): @click.argument('details', required=False) def power_assertion(lockdown: LockdownClient, type, name, timeout, details): """ Create a power assertion (wraps IOPMAssertionCreateWithName()) """ - PowerAssertionService(lockdown).create_power_assertion(type, name, timeout, details) + with PowerAssertionService(lockdown).create_power_assertion(type, name, timeout, details): + print('> Hit Ctrl+C to exit') + time.sleep(timeout) diff --git a/pymobiledevice3/services/power_assertion.py b/pymobiledevice3/services/power_assertion.py index df81f8ad8..a81e361e9 100755 --- a/pymobiledevice3/services/power_assertion.py +++ b/pymobiledevice3/services/power_assertion.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 - -import time +import contextlib from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.base_service import BaseService @@ -12,6 +11,7 @@ class PowerAssertionService(BaseService): def __init__(self, lockdown: LockdownClient): super().__init__(lockdown, self.SERVICE_NAME) + @contextlib.contextmanager def create_power_assertion(self, type_: str, name: str, timeout: int, details: str = None): msg = { 'CommandKey': 'CommandCreateAssertion', @@ -24,4 +24,4 @@ def create_power_assertion(self, type_: str, name: str, timeout: int, details: s msg['AssertionDetailKey'] = details self.service.send_recv_plist(msg) - time.sleep(timeout) + yield From 31430857df3c329a6b4a1930ab2ed97931220b76 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 16 Mar 2023 19:17:28 +0200 Subject: [PATCH 033/234] crash_reports: fix sysdiagnose creation on older devices --- pymobiledevice3/services/crash_reports.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index 4fef5a2f2..09de7b973 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -15,6 +15,7 @@ class CrashReportsManager: COPY_MOBILE_NAME = 'com.apple.crashreportcopymobile' CRASH_MOVER_NAME = 'com.apple.crashreportmover' APPSTORED_PATH = '/com.apple.appstored' + IN_PROGRESS_SYSDIAGNOSE_EXTENSIONS = ['.tmp', '.tar.gz'] def __init__(self, lockdown: LockdownClient): self.logger = logging.getLogger(__name__) @@ -127,9 +128,13 @@ def get_new_sysdiagnose(self, out: str, erase: bool = True) -> None: for filename in self.ls('DiagnosticLogs/sysdiagnose'): # search for an IN_PROGRESS archive if 'IN_PROGRESS_' in filename: - sysdiagnose_filename = filename.replace('IN_PROGRESS_', '') - sysdiagnose_filename = f'{posixpath.splitext(sysdiagnose_filename)[0]}.tar.gz' - break + for ext in self.IN_PROGRESS_SYSDIAGNOSE_EXTENSIONS: + if filename.endswith(ext): + sysdiagnose_filename = filename.rsplit(ext)[0] + sysdiagnose_filename = sysdiagnose_filename.replace('IN_PROGRESS_', '') + sysdiagnose_filename = f'{sysdiagnose_filename}.tar.gz' + print('filename', sysdiagnose_filename) + break break self.afc.wait_exists(sysdiagnose_filename) From 12176cd52049e2eddf5dcda6c7b2f032c51ae273 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 16 Mar 2023 19:28:45 +0200 Subject: [PATCH 034/234] setup: bump version to 1.40.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 61565ff80..7795eb711 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.39.0' +VERSION = '1.40.0' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From dec33a1fc9e79c74de928a5586f9df669cfd3d9b Mon Sep 17 00:00:00 2001 From: DoronZ Date: Thu, 30 Mar 2023 01:05:52 +0300 Subject: [PATCH 035/234] backup: fix encryption argument parsing --- pymobiledevice3/cli/backup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/cli/backup.py b/pymobiledevice3/cli/backup.py index 4dcdee8fc..56934925f 100644 --- a/pymobiledevice3/cli/backup.py +++ b/pymobiledevice3/cli/backup.py @@ -126,7 +126,7 @@ def extract(lockdown: LockdownClient, domain_name, relative_path, backup_directo @backup2.command(cls=Command) -@click.argument('mode', type=click.Choice(['ON', 'OFF'], case_sensitive=False)) +@click.argument('mode', type=click.Choice(['on', 'off'], case_sensitive=False)) @click.argument('password') @backup_directory_option def encryption(lockdown: LockdownClient, backup_directory, mode, password): @@ -137,7 +137,7 @@ def encryption(lockdown: LockdownClient, backup_directory, mode, password): When off, PASSWORD is the current backup password. """ backup_client = Mobilebackup2Service(lockdown) - should_encrypt = mode == 'ON' + should_encrypt = mode.lower() == 'on' if should_encrypt == backup_client.will_encrypt: logger.error('Encryption already ' + ('on!' if should_encrypt else 'off!')) return From f40055752c34412c01794140c6dd2ab84f38911e Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 3 Apr 2023 08:12:13 +0300 Subject: [PATCH 036/234] cli: remove redundant code from `wifi-connections` --- pymobiledevice3/cli/lockdown.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pymobiledevice3/cli/lockdown.py b/pymobiledevice3/cli/lockdown.py index dd610254b..2810ee1b4 100644 --- a/pymobiledevice3/cli/lockdown.py +++ b/pymobiledevice3/cli/lockdown.py @@ -4,7 +4,6 @@ import click from pymobiledevice3.cli.cli_common import Command, CommandWithoutAutopair, print_json -from pymobiledevice3.exceptions import PasscodeRequiredError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.heartbeat import HeartbeatService @@ -131,10 +130,4 @@ def lockdown_wifi_connections(lockdown: LockdownClient, state): print_json(lockdown.get_value(domain='com.apple.mobile.wireless_lockdown')) else: # enable/disable - state = state == 'on' - try: - # required when passcode is set, but cannot be set if not defined - lockdown.enable_wifi_pairing = state - except PasscodeRequiredError: - pass - lockdown.enable_wifi_connections = state + lockdown.enable_wifi_connections = state == 'on' From bb3589b9e02b43f6da7ae805c9f524f45f8a9a75 Mon Sep 17 00:00:00 2001 From: netanelc305 Date: Tue, 4 Apr 2023 02:54:56 -0700 Subject: [PATCH 037/234] installation_proxy: `get_apps_bid` return all app_types --- pymobiledevice3/services/installation_proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pymobiledevice3/services/installation_proxy.py b/pymobiledevice3/services/installation_proxy.py index e2f1b475a..1e512e906 100644 --- a/pymobiledevice3/services/installation_proxy.py +++ b/pymobiledevice3/services/installation_proxy.py @@ -166,8 +166,7 @@ def get_apps_bid(self, app_types=None): if app_types is None: app_types = ['User'] return [app['CFBundleIdentifier'] - for app in self.get_apps() - if app.get('ApplicationType') in app_types] + for app in self.get_apps(app_types)] def close(self): self.service.close() From eb6e96c6837c80596324e0bb65c698dedc2980d5 Mon Sep 17 00:00:00 2001 From: netanelc305 Date: Tue, 18 Apr 2023 04:42:29 -0700 Subject: [PATCH 038/234] mounter: fix auto_mount to verify versions --- pymobiledevice3/cli/mounter.py | 48 +++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/pymobiledevice3/cli/mounter.py b/pymobiledevice3/cli/mounter.py index c3e242eae..1108c04ce 100644 --- a/pymobiledevice3/cli/mounter.py +++ b/pymobiledevice3/cli/mounter.py @@ -1,9 +1,9 @@ +import json import logging import plistlib -import tempfile -import zipfile from pathlib import Path from typing import IO +from urllib.request import urlopen import click import requests @@ -15,7 +15,8 @@ from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.mobile_image_mounter import MobileImageMounterService -DEVELOPER_DISK_IMAGE_URL = 'https://github.com/pdso/DeveloperDiskImage/raw/master/{ios_version}/{ios_version}.zip' +DISK_IMAGE_TREE = "https://api.github.com/repos/pdso/DeveloperDiskImage/git/trees/master" +DEVELOPER_DISK_IMAGE_URL = 'https://github.com/pdso/DeveloperDiskImage/raw/master/{ios_version}/{file_name}' logger = logging.getLogger(__name__) @@ -90,12 +91,10 @@ def download_file(url, local_filename): return local_filename -def download_developer_disk_image(ios_version, directory): - url = DEVELOPER_DISK_IMAGE_URL.format(ios_version=ios_version) - with tempfile.NamedTemporaryFile('wb+') as f: - download_file(url, f.name) - zip_file = zipfile.ZipFile(f) - zip_file.extractall(directory) +def get_all_versions(): + data = urlopen(DISK_IMAGE_TREE).read() + json_data = json.loads(data) + return [item.get('path') for item in json_data.get('tree')][0:-3] @mounter.command('mount', cls=Command) @@ -157,17 +156,30 @@ def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): signature = f'{image_path}.signature' developer_disk_image_dir = Path(image_path).parent - if not developer_disk_image_dir.exists(): - try: - download_developer_disk_image(version, developer_disk_image_dir) - except PermissionError: - logger.error( - f'DeveloperDiskImage could not be saved to Xcode default path ({developer_disk_image_dir}). ' - f'Please make sure your user has the necessary permissions') - return - image_path = Path(image_path) signature = Path(signature) + + available_versions = get_all_versions() + + if version not in available_versions: + logger.error(f'Unable to find DeveloperDiskImage for {version}. available versions: {available_versions}') + return + + try: + developer_disk_image_dir.mkdir(exist_ok=True) + + if not image_path.exists(): + download_file(DEVELOPER_DISK_IMAGE_URL.format(ios_version=version, file_name=image_path.name), image_path) + + if not signature.exists(): + download_file(DEVELOPER_DISK_IMAGE_URL.format(ios_version=version, file_name=signature.name), signature) + + except PermissionError: + logger.error( + f'DeveloperDiskImage could not be saved to Xcode default path ({developer_disk_image_dir}). ' + f'Please make sure your user has the necessary permissions') + return + image_path = image_path.read_bytes() signature = signature.read_bytes() From b6de3facc8737c27cdff2957406785334dddf75d Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 19 Apr 2023 15:27:17 +0300 Subject: [PATCH 039/234] python-app: remove test usage on windows-latest/3.7 --- .github/workflows/python-app.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index e59c06af7..c2b787479 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -37,6 +37,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -U . - - name: Test show usage + - if: (!((matrix.os == 'windows-latest') && (matrix.python-version == '3.7'))) + name: Test show usage run: | python -m pymobiledevice3 From 8bf0b7aaf62d43bbad90ef57d5a525ce2dba386c Mon Sep 17 00:00:00 2001 From: Yotam Olenik Date: Sun, 2 Apr 2023 19:02:19 +0300 Subject: [PATCH 040/234] lockdown: add `host_id` option to `unpair()` --- pymobiledevice3/lockdown.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 8d2b78863..f6ed51b80 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -441,8 +441,13 @@ def pair(self, timeout: int = None) -> None: self.paired = True @reconnect_on_remote_close - def unpair(self) -> Mapping: - return self._request('Unpair', {'PairRecord': self.pair_record, 'ProtocolVersion': '2'}, verify_request=False) + def unpair(self, host_id: str = None) -> None: + if host_id is not None: + pair_record = {'HostID': host_id} + self._request('Unpair', {'PairRecord': pair_record, 'ProtocolVersion': '2'}, verify_request=False) + else: + self._request('Unpair', {'PairRecord': self.pair_record, 'ProtocolVersion': '2'}, + verify_request=False) @reconnect_on_remote_close def reset_pairing(self): From df70e83dee60ff4d94123bb9a712dc862a90a52e Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 10:17:42 +0300 Subject: [PATCH 041/234] exceptions: add `AppInstallError` --- pymobiledevice3/exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index bc41f2bbb..51f809f0b 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -261,3 +261,7 @@ class InspectorEvaluateError(PyMobileDevice3Exception): class LaunchingApplicationError(PyMobileDevice3Exception): pass + + +class AppInstallError(PyMobileDevice3Exception): + pass From fd686869a3419cad303a9fb2d81325e54e497f64 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 10:18:14 +0300 Subject: [PATCH 042/234] lockdown: fix indent --- pymobiledevice3/lockdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index f6ed51b80..1680b74c0 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -447,7 +447,7 @@ def unpair(self, host_id: str = None) -> None: self._request('Unpair', {'PairRecord': pair_record, 'ProtocolVersion': '2'}, verify_request=False) else: self._request('Unpair', {'PairRecord': self.pair_record, 'ProtocolVersion': '2'}, - verify_request=False) + verify_request=False) @reconnect_on_remote_close def reset_pairing(self): From 522af3020568b449d3c7b04cea1024ef0475296f Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 10:18:56 +0300 Subject: [PATCH 043/234] InstallationProxyService: refactor --- .../services/installation_proxy.py | 153 +++++++----------- 1 file changed, 59 insertions(+), 94 deletions(-) diff --git a/pymobiledevice3/services/installation_proxy.py b/pymobiledevice3/services/installation_proxy.py index 1e512e906..782b92d4e 100644 --- a/pymobiledevice3/services/installation_proxy.py +++ b/pymobiledevice3/services/installation_proxy.py @@ -1,6 +1,8 @@ import os import posixpath +from typing import Callable, List, Mapping +from pymobiledevice3.exceptions import AppInstallError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.afc import AfcService from pymobiledevice3.services.base_service import BaseService @@ -19,58 +21,68 @@ class InstallationProxyService(BaseService): def __init__(self, lockdown: LockdownClient): super().__init__(lockdown, self.SERVICE_NAME) - def watch_completion(self, handler=None, *args): + def _watch_completion(self, handler: Callable = None, *args) -> None: while True: response = self.service.recv_plist() if not response: break error = response.get('Error') if error: - raise IOError(f'''{error}: {response.get('ErrorDescription')}''') + raise AppInstallError(f'{error}: {response.get("ErrorDescription")}') completion = response.get('PercentComplete') if completion: if handler: self.logger.debug('calling handler') handler(completion, *args) - self.logger.info('%s %% Complete', response.get('PercentComplete')) + self.logger.info(f'{response.get("PercentComplete")}% Complete') if response.get('Status') == 'Complete': - return response.get('Status') - return 'Error' + return + raise AppInstallError() - def send_cmd_for_bid(self, bid, cmd='Archive', options=None, handler=None, *args): + def send_cmd_for_bundle_identifier(self, bundle_identifier: str, cmd: str = 'Archive', options: Mapping = None, + handler: Mapping = None, *args) -> None: + """ send a low-level command to installation relay """ cmd = {'Command': cmd, - 'ApplicationIdentifier': bid} - if options: - cmd.update({'ClientOptions': options}) + 'ApplicationIdentifier': bundle_identifier} + + if options is None: + options = {} + + cmd.update({'ClientOptions': options}) self.service.send_plist(cmd) - self.logger.info('%s : %s\n', cmd, self.watch_completion(handler, *args)) + self._watch_completion(handler, *args) + + def install(self, ipa_path: str, options: Mapping = None, handler: Callable = None, *args) -> None: + """ install given ipa from device path """ + self.install_from_local(ipa_path, 'Install', options, handler, args) - def uninstall(self, bid, options=None, handler=None, *args): - return self.send_cmd_for_bid(bid, 'Uninstall', options, handler, args) + def upgrade(self, ipa_path: str, options: Mapping = None, handler: Callable = None, *args) -> None: + """ upgrade given ipa from device path """ + self.install_from_local(ipa_path, 'Upgrade', options, handler, args) - def install_from_local(self, ipa_path, cmd='Install', options=None, handler=None, *args): + def restore(self, bundle_identifier: str, options: Mapping = None, handler: Callable = None, *args) -> None: + """ no longer supported on newer iOS versions """ + self.send_cmd_for_bundle_identifier(bundle_identifier, 'Restore', options, handler, args) + + def uninstall(self, bundle_identifier: str, options: Mapping = None, handler: Callable = None, *args) -> None: + """ uninstall given bundle_identifier """ + self.send_cmd_for_bundle_identifier(bundle_identifier, 'Uninstall', options, handler, args) + + def install_from_local(self, ipa_path: str, cmd='Install', options: Mapping = None, handler: Callable = None, + *args) -> None: + """ upload given ipa onto device and install it """ if options is None: options = {} remote_path = posixpath.join('/', os.path.basename(ipa_path)) - afc = AfcService(self.lockdown) - afc.set_file_contents(remote_path, open(ipa_path, 'rb').read()) + with AfcService(self.lockdown) as afc: + afc.set_file_contents(remote_path, open(ipa_path, 'rb').read()) cmd = {'Command': cmd, 'ClientOptions': options, 'PackagePath': remote_path} self.service.send_plist(cmd) - self.watch_completion(handler, args) - - def install(self, ipa_path, options=None, handler=None, *args): - if options is None: - options = {} - return self.install_from_local(ipa_path, 'Install', options, handler, args) + self._watch_completion(handler, args) - def upgrade(self, ipa_path, options=None, handler=None, *args): - if options is None: - options = {} - return self.install_from_local(ipa_path, 'Upgrade', options, handler, args) - - def check_capabilities_match(self, capabilities, options=None): + def check_capabilities_match(self, capabilities: Mapping = None, options: Mapping = None) -> Mapping: if options is None: options = {} cmd = {'Command': 'CheckCapabilitiesMatch', @@ -79,94 +91,47 @@ def check_capabilities_match(self, capabilities, options=None): if capabilities: cmd['Capabilities'] = capabilities - self.service.send_plist(cmd) - result = self.service.recv_plist().get('LookupResult') - return result + return self.service.send_recv_plist(cmd).get('LookupResult') - def browse(self, options=None, attributes=None): + def browse(self, bundle_identifier: str, options: Mapping = None, attributes: List[str] = None) -> List[Mapping]: if options is None: options = {} if attributes: options['ReturnAttributes'] = attributes cmd = {'Command': 'Browse', - 'ClientOptions': options} + 'ClientOptions': options, + 'ApplicationIdentifier': bundle_identifier} self.service.send_plist(cmd) result = [] while True: - z = self.service.recv_plist() - if not z: + response = self.service.recv_plist() + if not response: break - data = z.get('CurrentList') - if data: + data = response.get('CurrentList') + if data is not None: result += data - if z.get('Status') == 'Complete': + if response.get('Status') == 'Complete': break return result - def apps_info(self, options=None): - if options is None: - options = {} - cmd = {'Command': 'Lookup', - 'ClientOptions': options} - - self.service.send_plist(cmd) - return self.service.recv_plist().get('LookupResult') - - def archive(self, bid, options=None, handler=None, *args): - if options is None: - options = {} - self.send_cmd_for_bid(bid, 'Archive', options, handler, args) - - def restore_archive(self, bid, options=None, handler=None, *args): + def lookup(self, options: Mapping = None) -> Mapping: + """ search installation database """ if options is None: options = {} - self.send_cmd_for_bid(bid, 'Restore', options, handler, args) - - def remove_archive(self, bid, options=None, handler=None, *args): - if options is None: - options = {} - self.send_cmd_for_bid(bid, 'RemoveArchive', options, handler, args) - - def archives_info(self, options=None): - if options is None: - options = {} - cmd = {'Command': 'LookupArchive', - 'ClientOptions': options} + cmd = {'Command': 'Lookup', 'ClientOptions': options} return self.service.send_recv_plist(cmd).get('LookupResult') - def search_path_for_bid(self, bid): - path = None - for a in self.get_apps(app_types=['User', 'System']): - if a.get('CFBundleIdentifier') == bid: - path = a.get('Path') + '/' + a.get('CFBundleExecutable') - return path - - def get_apps(self, app_types=None): - if app_types is None: - app_types = ['User'] - return [app for app in self.apps_info().values() - if app.get('ApplicationType') in app_types] - - def print_apps(self, app_types=None): - if app_types is None: - app_types = ['User'] - for app in self.get_apps(app_types): - print(('%s : %s => %s' % (app.get('CFBundleDisplayName'), - app.get('CFBundleIdentifier'), - app.get('Path') if app.get('Path') - else app.get('Container'))).encode('utf-8')) - - def get_apps_bid(self, app_types=None): - if app_types is None: - app_types = ['User'] - return [app['CFBundleIdentifier'] - for app in self.get_apps(app_types)] - - def close(self): - self.service.close() + def get_apps(self, app_types: List[str] = None) -> List[Mapping]: + """ get applications according to given criteria """ + lookup_result = self.lookup().values() + result = [] + for app in lookup_result: + if (app_types is None) or (app['ApplicationType'] in app_types): + result.append(app) + return result From 9cee95e098a7fd2fcff8c9427d01e65973ed1d63 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 10:19:10 +0300 Subject: [PATCH 044/234] tests: add test_apps --- tests/services/test_apps.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/services/test_apps.py diff --git a/tests/services/test_apps.py b/tests/services/test_apps.py new file mode 100644 index 000000000..d827ab217 --- /dev/null +++ b/tests/services/test_apps.py @@ -0,0 +1,7 @@ +from pymobiledevice3.services.installation_proxy import InstallationProxyService + + +def test_get_apps(lockdown): + with InstallationProxyService(lockdown=lockdown) as installation_proxy: + apps = installation_proxy.get_apps() + assert len(apps) > 1 From da92b5020d45a5e750394e8dac235fd0ab79c4a1 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 15:31:56 +0300 Subject: [PATCH 045/234] requirements: add `zeroconf` --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a1295483a..f6a263e49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ inquirer pyimg4>=0.7 ipsw_parser>=1.1.2 remotezip +zeroconf From a748688b3d59b575d336fd57c6731893f4ea1717 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 17:53:14 +0300 Subject: [PATCH 046/234] InstallationProxyService: fix `browse()` --- pymobiledevice3/services/installation_proxy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/services/installation_proxy.py b/pymobiledevice3/services/installation_proxy.py index 782b92d4e..5aad5cccd 100644 --- a/pymobiledevice3/services/installation_proxy.py +++ b/pymobiledevice3/services/installation_proxy.py @@ -93,15 +93,14 @@ def check_capabilities_match(self, capabilities: Mapping = None, options: Mappin return self.service.send_recv_plist(cmd).get('LookupResult') - def browse(self, bundle_identifier: str, options: Mapping = None, attributes: List[str] = None) -> List[Mapping]: + def browse(self, options: Mapping = None, attributes: List[str] = None) -> List[Mapping]: if options is None: options = {} if attributes: options['ReturnAttributes'] = attributes cmd = {'Command': 'Browse', - 'ClientOptions': options, - 'ApplicationIdentifier': bundle_identifier} + 'ClientOptions': options} self.service.send_plist(cmd) From cd163c02f87408fbc934b939a70fcece7794988b Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 23 Apr 2023 15:32:23 +0300 Subject: [PATCH 047/234] add `bonjour` support --- README.md | 6 ++- pymobiledevice3/__main__.py | 3 +- pymobiledevice3/bonjour.py | 74 ++++++++++++++++++++++++++++++++++ pymobiledevice3/cli/bonjour.py | 41 +++++++++++++++++++ tests/services/test_bonjour.py | 8 ++++ 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 pymobiledevice3/bonjour.py create mode 100644 pymobiledevice3/cli/bonjour.py create mode 100644 tests/services/test_bonjour.py diff --git a/README.md b/README.md index f87ddd1b7..84f23fe2c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ both architecture and platform generic and is supported and tested on: Main features include: +* Device discovery over bonjour * TCP port forwarding * Viewing syslog lines (including debug) * Profile management @@ -110,6 +111,7 @@ Commands: amfi amfi options apps application options backup2 backup utils + bonjour bonjour options companion companion options crash crash report options developer developer options. @@ -125,7 +127,7 @@ Commands: restore restore options springboard springboard options syslog syslog options - usbmuxd usbmuxd options + usbmux usbmuxd options webinspector webinspector options ``` @@ -150,6 +152,8 @@ There is A LOT you may do on the device using `pymobiledevice3`. This is just a * Listing connected devices: * `pymobiledevice3 list-devices` +* Discover network devices using bonjour: + * `pymobiledevice3 bonjour browse` * View all syslog lines (including debug messages): * `pymobiledevice3 syslog live` * Filter out only messages containing the word "SpringBoard": diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index 1d556e477..75e762985 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -10,6 +10,7 @@ from pymobiledevice3.cli.amfi import cli as amfi_cli from pymobiledevice3.cli.apps import cli as apps_cli from pymobiledevice3.cli.backup import cli as backup_cli +from pymobiledevice3.cli.bonjour import cli as bonjour_cli from pymobiledevice3.cli.companion_proxy import cli as companion_cli from pymobiledevice3.cli.crash import cli as crash_cli from pymobiledevice3.cli.developer import cli as developer_cli @@ -49,7 +50,7 @@ def cli(): cli_commands = click.CommandCollection(sources=[ developer_cli, mounter_cli, apps_cli, profile_cli, lockdown_cli, diagnostics_cli, syslog_cli, pcap_cli, crash_cli, afc_cli, ps_cli, notification_cli, usbmux_cli, power_assertion_cli, springboard_cli, - provision_cli, backup_cli, restore_cli, activation_cli, companion_cli, webinspector_cli, amfi_cli + provision_cli, backup_cli, restore_cli, activation_cli, companion_cli, webinspector_cli, amfi_cli, bonjour_cli ]) cli_commands.context_settings = dict(help_option_names=['-h', '--help']) try: diff --git a/pymobiledevice3/bonjour.py b/pymobiledevice3/bonjour.py new file mode 100644 index 000000000..99fef77aa --- /dev/null +++ b/pymobiledevice3/bonjour.py @@ -0,0 +1,74 @@ +import dataclasses +import logging +import socket +import time +from typing import List, Mapping + +from zeroconf import InterfaceChoice, InterfacesType, IPVersion, ServiceBrowser, ServiceListener, Zeroconf + +from pymobiledevice3.lockdown import LockdownClient + +SERVICE_NAME = '_apple-mobdev2._tcp.local.' + +logger = logging.getLogger(__name__) + + +@dataclasses.dataclass +class BonjourDevice: + name: str + mac_address: str + ipv4: List[str] + ipv6: List[str] + lockdown: LockdownClient + + def asdict(self) -> Mapping: + return { + 'name': self.name, + 'mac_address': self.mac_address, + 'ipv4': self.ipv4, + 'ipv6': self.ipv6, + 'lockdown_info': self.lockdown.all_values + } + + +class BonjourListener(ServiceListener): + def __init__(self, pair_records: List[Mapping] = None): + super().__init__() + self.pair_records = [] if pair_records is None else pair_records + self.discovered_devices = {} + + def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: + logger.debug(f'Service {name} updated') + + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + logger.debug(f'Service {name} removed') + self.discovered_devices.pop(name) + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + info = zc.get_service_info(type_, name) + logger.debug(f'Service {name} added, service info: {info}') + + ipv4 = [socket.inet_ntop(socket.AF_INET, address) for address in info.addresses_by_version(IPVersion.V4Only)] + ipv6 = [socket.inet_ntop(socket.AF_INET6, address) for address in info.addresses_by_version(IPVersion.V6Only)] + + try: + lockdown = LockdownClient(hostname=ipv4[0], autopair=False) + + for pair_record in self.pair_records: + lockdown = LockdownClient(hostname=ipv4[0], autopair=False, pair_record=pair_record) + if lockdown.paired: + break + except ConnectionRefusedError: + return + + self.discovered_devices[name] = BonjourDevice(name=name, mac_address=name.split('@')[0], ipv4=ipv4, ipv6=ipv6, + lockdown=lockdown) + + +def browse(timeout: int, interfaces: InterfacesType = InterfaceChoice.All, pair_records: List[Mapping] = None) -> \ + Mapping[str, BonjourDevice]: + with Zeroconf(interfaces=interfaces) as zc: + listener = BonjourListener(pair_records=pair_records) + ServiceBrowser(zc, SERVICE_NAME, listener) + time.sleep(timeout) + return listener.discovered_devices diff --git a/pymobiledevice3/cli/bonjour.py b/pymobiledevice3/cli/bonjour.py new file mode 100644 index 000000000..ef4b86d67 --- /dev/null +++ b/pymobiledevice3/cli/bonjour.py @@ -0,0 +1,41 @@ +import plistlib +from pathlib import Path + +import click + +from pymobiledevice3.bonjour import browse +from pymobiledevice3.cli.cli_common import print_json + +DEFAULT_BROWSE_TIMEOUT = 5 + + +@click.group() +def cli(): + """ bonjour cli """ + pass + + +@cli.group('bonjour') +def bonjour_cli(): + """ bonjour options """ + pass + + +@bonjour_cli.command('browse') +@click.option('--timeout', default=DEFAULT_BROWSE_TIMEOUT, type=click.INT) +@click.option('--pair-records', type=click.Path(dir_okay=True, file_okay=False, exists=True), + help='pair records to attempt validation with') +@click.option('--color/--no-color', default=True) +def cli_browse(timeout: int, pair_records: str, color: bool): + """ browse devices over bonjour """ + records = [] + if pair_records is not None: + for record in Path(pair_records).glob('*.plist'): + records.append(plistlib.loads(record.read_bytes())) + + output = [] + for device in browse(timeout, pair_records=records).values(): + device = device.asdict() + output.append(device) + + print_json(output, colored=color) diff --git a/tests/services/test_bonjour.py b/tests/services/test_bonjour.py new file mode 100644 index 000000000..892c72d0c --- /dev/null +++ b/tests/services/test_bonjour.py @@ -0,0 +1,8 @@ +from pymobiledevice3.bonjour import browse + +BROWSE_TIMEOUT = 1 + + +def test_bonjour(lockdown): + lockdown.enable_wifi_connections = True + assert len(browse(BROWSE_TIMEOUT).keys()) >= 1 From 86b6a766580308eedcc5de0bd61eac360ddded93 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 24 Apr 2023 07:54:42 +0300 Subject: [PATCH 048/234] requirements: pygnuutils>=0.0.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6a263e49..052476e2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ cached-property requests cmd2 packaging -pygnuutils>=0.0.6 +pygnuutils>=0.0.7 cryptography>=35.0.0 pycrashreport>=1.0.6 fastapi[all] From 409e93e8b32be3f36c659fe18801468029e10d9a Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 24 Apr 2023 09:21:34 +0300 Subject: [PATCH 049/234] mobilebackup2: use context-manager to fix unclosed fds --- pymobiledevice3/services/mobilebackup2.py | 130 +++++++++++----------- 1 file changed, 63 insertions(+), 67 deletions(-) diff --git a/pymobiledevice3/services/mobilebackup2.py b/pymobiledevice3/services/mobilebackup2.py index e78f25d2b..720a88d45 100755 --- a/pymobiledevice3/services/mobilebackup2.py +++ b/pymobiledevice3/services/mobilebackup2.py @@ -53,10 +53,9 @@ def backup(self, full: bool = True, backup_directory='.', progress_callback=lamb device_directory = backup_directory / self.lockdown.udid device_directory.mkdir(exist_ok=True, mode=0o755, parents=True) - with self.device_link(backup_directory) as dl: - notification_proxy = NotificationProxyService(self.lockdown) - afc = AfcService(self.lockdown) - + with self.device_link(backup_directory) as dl, \ + NotificationProxyService(self.lockdown) as notification_proxy, \ + AfcService(self.lockdown) as afc: with self._backup_lock(afc, notification_proxy): # Initialize Info.plist info_plist = self.init_mobile_backup_factory_info(afc) @@ -105,10 +104,9 @@ def restore(self, backup_directory='.', system: bool = False, reboot: bool = Tru source = source if source else self.lockdown.udid self._assert_backup_exists(backup_directory, source) - with self.device_link(backup_directory) as dl: - notification_proxy = NotificationProxyService(self.lockdown) - afc = AfcService(self.lockdown) - + with self.device_link(backup_directory) as dl, \ + NotificationProxyService(self.lockdown) as notification_proxy, \ + AfcService(self.lockdown) as afc: with self._backup_lock(afc, notification_proxy): manifest_plist_path = backup_directory / source / 'Manifest.plist' with open(manifest_plist_path, 'rb') as fd: @@ -253,71 +251,69 @@ def version_exchange(self, dl: DeviceLink, local_versions=None) -> None: assert reply[1]['ProtocolVersion'] in local_versions def init_mobile_backup_factory_info(self, afc: AfcService): - ip = InstallationProxyService(self.lockdown) - sbs = SpringBoardServicesService(self.lockdown) + with InstallationProxyService(self.lockdown) as ip, SpringBoardServicesService(self.lockdown) as sbs: + root_node = self.lockdown.get_value() + itunes_settings = self.lockdown.get_value(domain='com.apple.iTunes') + min_itunes_version = self.lockdown.get_value('com.apple.mobile.iTunes', 'MinITunesVersion') + app_dict = {} + installed_apps = [] + apps = ip.browse(options={'ApplicationType': 'User'}, + attributes=['CFBundleIdentifier', 'ApplicationSINF', 'iTunesMetadata']) + for app in apps: + bundle_id = app['CFBundleIdentifier'] + if bundle_id: + installed_apps.append(bundle_id) + if app.get('iTunesMetadata', False) and app.get('ApplicationSINF', False): + app_dict[bundle_id] = { + 'ApplicationSINF': app['ApplicationSINF'], + 'iTunesMetadata': app['iTunesMetadata'], + 'PlaceholderIcon': sbs.get_icon_pngdata(bundle_id), + } + + files = {} + for file in ITUNES_FILES: + try: + data_buf = afc.get_file_contents('/iTunes_Control/iTunes/' + file) + except AfcFileNotFoundError: + pass + else: + files[file] = data_buf - root_node = self.lockdown.get_value() - itunes_settings = self.lockdown.get_value(domain='com.apple.iTunes') - min_itunes_version = self.lockdown.get_value('com.apple.mobile.iTunes', 'MinITunesVersion') - app_dict = {} - installed_apps = [] - apps = ip.browse(options={'ApplicationType': 'User'}, - attributes=['CFBundleIdentifier', 'ApplicationSINF', 'iTunesMetadata']) - for app in apps: - bundle_id = app['CFBundleIdentifier'] - if bundle_id: - installed_apps.append(bundle_id) - if app.get('iTunesMetadata', False) and app.get('ApplicationSINF', False): - app_dict[bundle_id] = { - 'ApplicationSINF': app['ApplicationSINF'], - 'iTunesMetadata': app['iTunesMetadata'], - 'PlaceholderIcon': sbs.get_icon_pngdata(bundle_id), - } + ret = { + 'iTunes Version': min_itunes_version if min_itunes_version else '10.0.1', + 'iTunes Files': files, + 'Unique Identifier': self.lockdown.udid.upper(), + 'Target Type': 'Device', + 'Target Identifier': root_node['UniqueDeviceID'], + 'Serial Number': root_node['SerialNumber'], + 'Product Version': root_node['ProductVersion'], + 'Product Type': root_node['ProductType'], + 'Installed Applications': installed_apps, + 'GUID': uuid.uuid4().bytes, + 'Display Name': root_node['DeviceName'], + 'Device Name': root_node['DeviceName'], + 'Build Version': root_node['BuildVersion'], + 'Applications': app_dict, + } + + if 'IntegratedCircuitCardIdentity' in root_node: + ret['ICCID'] = root_node['IntegratedCircuitCardIdentity'] + if 'InternationalMobileEquipmentIdentity' in root_node: + ret['IMEI'] = root_node['InternationalMobileEquipmentIdentity'] + if 'MobileEquipmentIdentifier' in root_node: + ret['MEID'] = root_node['MobileEquipmentIdentifier'] + if 'PhoneNumber' in root_node: + ret['Phone Number'] = root_node['PhoneNumber'] - files = {} - for file in ITUNES_FILES: try: - data_buf = afc.get_file_contents('/iTunes_Control/iTunes/' + file) + data_buf = afc.get_file_contents('/Books/iBooksData2.plist') except AfcFileNotFoundError: pass else: - files[file] = data_buf - - ret = { - 'iTunes Version': min_itunes_version if min_itunes_version else '10.0.1', - 'iTunes Files': files, - 'Unique Identifier': self.lockdown.udid.upper(), - 'Target Type': 'Device', - 'Target Identifier': root_node['UniqueDeviceID'], - 'Serial Number': root_node['SerialNumber'], - 'Product Version': root_node['ProductVersion'], - 'Product Type': root_node['ProductType'], - 'Installed Applications': installed_apps, - 'GUID': uuid.uuid4().bytes, - 'Display Name': root_node['DeviceName'], - 'Device Name': root_node['DeviceName'], - 'Build Version': root_node['BuildVersion'], - 'Applications': app_dict, - } - - if 'IntegratedCircuitCardIdentity' in root_node: - ret['ICCID'] = root_node['IntegratedCircuitCardIdentity'] - if 'InternationalMobileEquipmentIdentity' in root_node: - ret['IMEI'] = root_node['InternationalMobileEquipmentIdentity'] - if 'MobileEquipmentIdentifier' in root_node: - ret['MEID'] = root_node['MobileEquipmentIdentifier'] - if 'PhoneNumber' in root_node: - ret['Phone Number'] = root_node['PhoneNumber'] - - try: - data_buf = afc.get_file_contents('/Books/iBooksData2.plist') - except AfcFileNotFoundError: - pass - else: - ret['iBooks Data 2'] = data_buf - if itunes_settings: - ret['iTunes Settings'] = itunes_settings - return ret + ret['iBooks Data 2'] = data_buf + if itunes_settings: + ret['iTunes Settings'] = itunes_settings + return ret @contextmanager def _backup_lock(self, afc, notification_proxy): From 11da8532966a5369cbb40292a2e5d7ed20fcf3cb Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 24 Apr 2023 09:22:30 +0300 Subject: [PATCH 050/234] tests: add test_backup2 --- tests/services/test_backup2.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/services/test_backup2.py diff --git a/tests/services/test_backup2.py b/tests/services/test_backup2.py new file mode 100644 index 000000000..80fed1a78 --- /dev/null +++ b/tests/services/test_backup2.py @@ -0,0 +1,30 @@ +import time +from ssl import SSLEOFError + +from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service + +PASSWORD = '1234' + + +def change_password(lockdown, old: str = '', new: str = '') -> None: + while True: + try: + with Mobilebackup2Service(lockdown) as service: + service.change_password(old=old, new=new) + except (SSLEOFError, ConnectionAbortedError): + # after large backups, the device requires time to recover + time.sleep(1) + else: + break + + +def test_backup(lockdown, tmp_path): + with Mobilebackup2Service(lockdown) as service: + service.backup(full=True, backup_directory=tmp_path) + + +def test_encrypted_backup(lockdown, tmp_path): + change_password(lockdown, new=PASSWORD) + with Mobilebackup2Service(lockdown) as service: + service.backup(full=True, backup_directory=tmp_path) + change_password(lockdown, old=PASSWORD) From 7bbb98faa8941120f44dd6c292951d45e92cf18d Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 24 Apr 2023 09:28:27 +0300 Subject: [PATCH 051/234] test_webinspector: add `TIMEOUT` on `connect()` --- tests/services/test_webinspector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/services/test_webinspector.py b/tests/services/test_webinspector.py index 7836acfd7..b2952f8ae 100644 --- a/tests/services/test_webinspector.py +++ b/tests/services/test_webinspector.py @@ -1,9 +1,11 @@ from pymobiledevice3.services.webinspector import SAFARI, WebinspectorService +TIMEOUT = 10 + def test_opening_app(lockdown): inspector = WebinspectorService(lockdown=lockdown) - inspector.connect() + inspector.connect(timeout=TIMEOUT) safari = inspector.open_app(SAFARI) pages = inspector.get_open_pages() # Might take a while to update. From 84e8b0c1fe0a0b9e37247517e70960c31dc27460 Mon Sep 17 00:00:00 2001 From: Yotam Olenik Date: Mon, 24 Apr 2023 10:25:21 +0300 Subject: [PATCH 052/234] bonjour: add a debug print when connection refused --- pymobiledevice3/bonjour.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymobiledevice3/bonjour.py b/pymobiledevice3/bonjour.py index 99fef77aa..250c83a6d 100644 --- a/pymobiledevice3/bonjour.py +++ b/pymobiledevice3/bonjour.py @@ -59,6 +59,7 @@ def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: if lockdown.paired: break except ConnectionRefusedError: + logger.debug('Service failed to establish a lockdown connection') return self.discovered_devices[name] = BonjourDevice(name=name, mac_address=name.split('@')[0], ipv4=ipv4, ipv6=ipv6, From 6939d73e448d31c6fdb736cdc99fc804bf310cf9 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 24 Apr 2023 16:19:46 +0300 Subject: [PATCH 053/234] setup: bump version to 1.41.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7795eb711..8e38ffef9 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.40.0' +VERSION = '1.41.0' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From 68a9020876203e9f3f2d454ce330558d6d21b95d Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 27 Apr 2023 09:15:58 +0300 Subject: [PATCH 054/234] installation_proxy: remove redundant code --- pymobiledevice3/services/installation_proxy.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pymobiledevice3/services/installation_proxy.py b/pymobiledevice3/services/installation_proxy.py index 5aad5cccd..99851477f 100644 --- a/pymobiledevice3/services/installation_proxy.py +++ b/pymobiledevice3/services/installation_proxy.py @@ -7,13 +7,6 @@ from pymobiledevice3.services.afc import AfcService from pymobiledevice3.services.base_service import BaseService -client_options = { - 'SkipUninstall': False, - 'ApplicationSINF': False, - 'iTunesMetadata': False, - 'ReturnAttributes': False -} - class InstallationProxyService(BaseService): SERVICE_NAME = 'com.apple.mobile.installation_proxy' From 3925e0e22c7784c350b5953dbbd14eb0c72ad250 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 27 Apr 2023 09:16:33 +0300 Subject: [PATCH 055/234] InstallationProxyService: make `get_apps()` return a Mapping including app sizes (#415) --- .../services/installation_proxy.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pymobiledevice3/services/installation_proxy.py b/pymobiledevice3/services/installation_proxy.py index 99851477f..cb2e3af2f 100644 --- a/pymobiledevice3/services/installation_proxy.py +++ b/pymobiledevice3/services/installation_proxy.py @@ -7,6 +7,8 @@ from pymobiledevice3.services.afc import AfcService from pymobiledevice3.services.base_service import BaseService +GET_APPS_ADDITIONAL_INFO = {'ReturnAttributes': ['CFBundleIdentifier', 'StaticDiskUsage', 'DynamicDiskUsage']} + class InstallationProxyService(BaseService): SERVICE_NAME = 'com.apple.mobile.installation_proxy' @@ -119,11 +121,16 @@ def lookup(self, options: Mapping = None) -> Mapping: cmd = {'Command': 'Lookup', 'ClientOptions': options} return self.service.send_recv_plist(cmd).get('LookupResult') - def get_apps(self, app_types: List[str] = None) -> List[Mapping]: + def get_apps(self, app_types: List[str] = None) -> Mapping[str, Mapping]: """ get applications according to given criteria """ - lookup_result = self.lookup().values() - result = [] - for app in lookup_result: + result = self.lookup() + # query for additional info + additional_info = self.lookup(GET_APPS_ADDITIONAL_INFO) + for bundle_identifier, app in additional_info.items(): + result[bundle_identifier].update(app) + # filter results + filtered_result = {} + for bundle_identifier, app in result.items(): if (app_types is None) or (app['ApplicationType'] in app_types): - result.append(app) - return result + filtered_result[bundle_identifier] = app + return filtered_result From 54cf1d9233c4fa58ca7d7d6e9b5bf4e8227fa731 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 27 Apr 2023 10:59:26 +0300 Subject: [PATCH 056/234] cli: add `--hidden` for `apps list` --- pymobiledevice3/cli/apps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/apps.py b/pymobiledevice3/cli/apps.py index 93b938641..af6babb48 100644 --- a/pymobiledevice3/cli/apps.py +++ b/pymobiledevice3/cli/apps.py @@ -22,13 +22,16 @@ def apps(): @click.option('--color/--no-color', default=True) @click.option('-u', '--user', is_flag=True, help='include user apps') @click.option('-s', '--system', is_flag=True, help='include system apps') -def apps_list(lockdown: LockdownClient, color, user, system): +@click.option('--hidden', is_flag=True, help='include hidden apps') +def apps_list(lockdown: LockdownClient, color, user, system, hidden): """ list installed apps """ app_types = [] if user: app_types.append('User') if system: app_types.append('System') + if hidden: + app_types.append('Hidden') print_json(InstallationProxyService(lockdown=lockdown).get_apps(app_types), colored=color) From a40db974803417ae2053254ab0a7f2b480448f5b Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sat, 29 Apr 2023 12:32:06 +0300 Subject: [PATCH 057/234] README: fix list devices command (#445) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84f23fe2c..be9bd604d 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ https://terminalizer.com/view/18920b405193 There is A LOT you may do on the device using `pymobiledevice3`. This is just a TL;DR of some common operations: * Listing connected devices: - * `pymobiledevice3 list-devices` + * `pymobiledevice3 usbmux list` * Discover network devices using bonjour: * `pymobiledevice3 bonjour browse` * View all syslog lines (including debug messages): From 5ff96a88eb89c222053267b1542f818d00e9b401 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 4 May 2023 11:36:21 +0300 Subject: [PATCH 058/234] cli: make `automount` version query just log if failed --- pymobiledevice3/cli/mounter.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pymobiledevice3/cli/mounter.py b/pymobiledevice3/cli/mounter.py index 1108c04ce..6b861650a 100644 --- a/pymobiledevice3/cli/mounter.py +++ b/pymobiledevice3/cli/mounter.py @@ -2,7 +2,8 @@ import logging import plistlib from pathlib import Path -from typing import IO +from typing import IO, List +from urllib.error import URLError from urllib.request import urlopen import click @@ -15,7 +16,7 @@ from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.mobile_image_mounter import MobileImageMounterService -DISK_IMAGE_TREE = "https://api.github.com/repos/pdso/DeveloperDiskImage/git/trees/master" +DISK_IMAGE_TREE = 'https://api.github.com/repos/pdso/DeveloperDiskImage/git/trees/master' DEVELOPER_DISK_IMAGE_URL = 'https://github.com/pdso/DeveloperDiskImage/raw/master/{ios_version}/{file_name}' logger = logging.getLogger(__name__) @@ -91,7 +92,7 @@ def download_file(url, local_filename): return local_filename -def get_all_versions(): +def get_all_versions() -> List[str]: data = urlopen(DISK_IMAGE_TREE).read() json_data = json.loads(data) return [item.get('path') for item in json_data.get('tree')][0:-3] @@ -159,11 +160,15 @@ def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): image_path = Path(image_path) signature = Path(signature) - available_versions = get_all_versions() - - if version not in available_versions: - logger.error(f'Unable to find DeveloperDiskImage for {version}. available versions: {available_versions}') - return + if not image_path.exists(): + try: + available_versions = get_all_versions() + if version not in available_versions: + logger.error( + f'Unable to find DeveloperDiskImage for {version}. available versions: {available_versions}') + return + except URLError: + logger.warning('failed to query DeveloperDiskImage versions') try: developer_disk_image_dir.mkdir(exist_ok=True) From 2a09d4df51dd4d28a3b1d8bc67892b12f17ca3f2 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 4 May 2023 11:43:54 +0300 Subject: [PATCH 059/234] setup: bump version to 1.42.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e38ffef9..6e22db04b 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import find_packages, setup BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.41.0' +VERSION = '1.42.0' PACKAGE_NAME = 'pymobiledevice3' PACKAGES = [p for p in find_packages() if not p.startswith('tests')] From b1baab1af083abe22971db50d354bed08b1cffbf Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 7 May 2023 08:58:38 +0300 Subject: [PATCH 060/234] crash_reports: remove `print()` from sysdiagnose creation --- pymobiledevice3/services/crash_reports.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index 09de7b973..528844094 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -133,7 +133,6 @@ def get_new_sysdiagnose(self, out: str, erase: bool = True) -> None: sysdiagnose_filename = filename.rsplit(ext)[0] sysdiagnose_filename = sysdiagnose_filename.replace('IN_PROGRESS_', '') sysdiagnose_filename = f'{sysdiagnose_filename}.tar.gz' - print('filename', sysdiagnose_filename) break break From bd5ad89b7801d8017e28258cb17ab9549d96dd4f Mon Sep 17 00:00:00 2001 From: DoronZ Date: Mon, 8 May 2023 20:03:51 +0300 Subject: [PATCH 061/234] cli: refactor `webinspector` docstrings (#450) --- pymobiledevice3/cli/webinspector.py | 33 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pymobiledevice3/cli/webinspector.py b/pymobiledevice3/cli/webinspector.py index 39c3179c1..a3941f832 100644 --- a/pymobiledevice3/cli/webinspector.py +++ b/pymobiledevice3/cli/webinspector.py @@ -77,9 +77,10 @@ def create_webinspector_and_launch_app(lockdown: LockdownClient, timeout: float, @catch_errors def opened_tabs(lockdown: LockdownClient, verbose, timeout): """ - Show All opened tabs. - Opt in: + Show all currently opened tabs. + \b + Opt-in: Settings -> Safari -> Advanced -> Web Inspector """ inspector = WebinspectorService(lockdown=lockdown, loop=asyncio.get_event_loop()) @@ -108,11 +109,11 @@ def opened_tabs(lockdown: LockdownClient, verbose, timeout): @catch_errors def launch(lockdown: LockdownClient, url, timeout): """ - Open a specific URL in Safari. - Opt in: + Create a specific URL in Safari. + \b + Opt-in: Settings -> Safari -> Advanced -> Web Inspector - Settings -> Safari -> Advanced -> Remote Automation """ inspector, safari = create_webinspector_and_launch_app(lockdown, timeout, SAFARI) @@ -153,10 +154,11 @@ def launch(lockdown: LockdownClient, url, timeout): @catch_errors def shell(lockdown: LockdownClient, timeout): """ - Opt in: + Create an IPython shell for interacting with a WebView. + \b + Opt-in: Settings -> Safari -> Advanced -> Web Inspector - Settings -> Safari -> Advanced -> Remote Automation """ inspector, safari = create_webinspector_and_launch_app(lockdown, timeout, SAFARI) @@ -182,12 +184,15 @@ def shell(lockdown: LockdownClient, timeout): @catch_errors def js_shell(lockdown: LockdownClient, timeout, automation, url): """ - Opt in: + Create a javascript shell. This interpreter runs on your local machine, + but evaluates each expression on the remote + \b + Opt-in: Settings -> Safari -> Advanced -> Web Inspector + \b for automation also enable: - Settings -> Safari -> Advanced -> Remote Automation """ @@ -208,6 +213,13 @@ def create_app(): @click.option('--host', default='127.0.0.1') @click.option('--port', type=click.INT, default=9222) def cdp(lockdown: LockdownClient, host, port): + """ + Start a CDP server for debugging WebViews. + + \b + In order to debug the WebView that way, open in Google Chrome: + chrome://inspect/#devices + """ global udid udid = lockdown.udid uvicorn.run('pymobiledevice3.cli.webinspector:create_app', host=host, port=port, factory=True, @@ -215,7 +227,6 @@ def cdp(lockdown: LockdownClient, host, port): class JsShell(ABC): - def __init__(self): super().__init__() self.prompt_session = PromptSession(lexer=PygmentsLexer(lexers.JavascriptLexer), @@ -267,7 +278,6 @@ def webinspector_history_path() -> str: class AutomationJsShell(JsShell): - def __init__(self, driver: WebDriver): super().__init__() self.driver = driver @@ -293,7 +303,6 @@ async def navigate(self, url: str): class InspectorJsShell(JsShell): - def __init__(self, inspector_session: InspectorSession): super().__init__() self.inspector_session = inspector_session From 0f0954b0853b381d85204716239405f24322c6f5 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 11 May 2023 13:11:22 +0300 Subject: [PATCH 062/234] use `pyproject.toml` instead of `setup.py` --- pyproject.toml | 51 ++++++++++++++++++++++++++++++++++++++++++++ setup.py | 57 -------------------------------------------------- 2 files changed, 51 insertions(+), 57 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..552d4e6d5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[project] +name = "pymobiledevice3" +version = "1.42.0" +description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" +readme = "README.md" +requires-python = ">=3.7" +license = { file = "LICENSE" } +keywords = ["ios", "protocol", "lockdownd", "instruments", "automation", "cli", "afc"] +authors = [ + { name = "doronz88", email = "doron88@gmail.com" }, + { name = "matan", email = "matan1008@gmail.com" } +] +maintainers = [ + { name = "doronz88", email = "doron88@gmail.com" }, + { name = "matan", email = "matan1008@gmail.com" } +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", +] +dynamic = ["dependencies"] + +[project.optional-dependencies] +test = ["pytest", "cmd2_ext_test"] + +[project.urls] +"Homepage" = "https://github.com/doronz88/pymobiledevice3" +"Bug Reports" = "https://github.com/doronz88/pymobiledevice3/issues" + +[project.scripts] +pymobiledevice3 = "pymobiledevice3.__main__:cli" + +[tool.setuptools] +package-data = { "pymobiledevice3" = ["resources/webinspector/*.js", "resources/dsc_uuid_map.json", "resources/notifications.txt"] } + +[tool.setuptools.packages.find] +exclude = ["docs*", "tests*"] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } + +[build-system] +requires = ["setuptools>=43.0.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py deleted file mode 100644 index 6e22db04b..000000000 --- a/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -from pathlib import Path - -from setuptools import find_packages, setup - -BASE_DIR = Path(__file__).parent.resolve(strict=True) -VERSION = '1.42.0' -PACKAGE_NAME = 'pymobiledevice3' -PACKAGES = [p for p in find_packages() if not p.startswith('tests')] - - -def parse_requirements(): - reqs = [] - with open(BASE_DIR / 'requirements.txt', 'r') as fd: - for line in fd.readlines(): - line = line.strip() - if line: - reqs.append(line) - return reqs - - -def get_description(): - return (BASE_DIR / 'README.md').read_text() - - -if __name__ == '__main__': - setup( - version=VERSION, - name=PACKAGE_NAME, - description='Pure python3 implementation for working with iDevices (iPhone, etc...)', - long_description=get_description(), - long_description_content_type='text/markdown', - cmdclass={}, - packages=PACKAGES, - include_package_data=True, - package_data={PACKAGE_NAME: ['resources/webinspector/*.js', - 'resources/dsc_uuid_map.json', - 'resources/notifications.txt']}, - author='DoronZ', - author_email='doron88@gmail.com', - license='GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007', - install_requires=parse_requirements(), - entry_points={ - 'console_scripts': ['pymobiledevice3=pymobiledevice3.__main__:cli', - ], - }, - classifiers=[ - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - ], - url='https://github.com/doronz88/pymobiledevice3', - project_urls={ - 'pymobiledevice3': 'https://github.com/doronz88/pymobiledevice3' - }, - tests_require=['pytest', 'cmd2_ext_test'], - ) From b438f7090908ad54ac693f5cb7fc22005761c336 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 11 May 2023 13:12:56 +0300 Subject: [PATCH 063/234] python-publish: use `build` package --- .github/workflows/python-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3bd2eb895..c007f70ec 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -21,11 +21,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install -U build setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel + python -m build twine upload dist/* \ No newline at end of file From a24a7d8a31160d139e0dd96056ee153cbd6bfa14 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Thu, 11 May 2023 23:19:13 +0300 Subject: [PATCH 064/234] pyproject: bump version to 1.42.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 552d4e6d5..9be57e5b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "1.42.0" +version = "1.42.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.7" From 9cd208bfff129cfc760ef33f9a3cbad7ad25ecbc Mon Sep 17 00:00:00 2001 From: DoronZ Date: Thu, 11 May 2023 23:25:55 +0300 Subject: [PATCH 065/234] pyproject: fix gplv3 classifier --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9be57e5b1..fc4605884 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ maintainers = [ ] classifiers = [ "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 5c61060fdf5f97dedd84d37acb77ee6aa9ca8c4e Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 18 May 2023 08:27:56 +0300 Subject: [PATCH 066/234] amfi: also handle `BadDevError` from `enable_developer_mode()` --- pymobiledevice3/services/amfi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/services/amfi.py b/pymobiledevice3/services/amfi.py index 43b4c3e80..d53c3f50e 100644 --- a/pymobiledevice3/services/amfi.py +++ b/pymobiledevice3/services/amfi.py @@ -3,7 +3,7 @@ import construct -from pymobiledevice3.exceptions import AmfiError, ConnectionFailedError, DeveloperModeError, \ +from pymobiledevice3.exceptions import AmfiError, BadDevError, ConnectionFailedError, DeveloperModeError, \ DeviceHasPasscodeSetError, NoDeviceConnectedError, PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.heartbeat import HeartbeatService @@ -53,7 +53,7 @@ def enable_developer_mode(self, enable_post_restart=True): try: self._lockdown = LockdownClient(self._lockdown.udid) break - except (NoDeviceConnectedError, ConnectionFailedError, construct.core.StreamError): + except (NoDeviceConnectedError, ConnectionFailedError, BadDevError, construct.core.StreamError): pass self.enable_developer_mode_post_restart() From c0514d17e5c64ad418c99c86e7bda36221c2fec8 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Thu, 18 May 2023 17:12:32 +0300 Subject: [PATCH 067/234] pyproject: bump version to 1.42.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fc4605884..14d041e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "1.42.1" +version = "1.42.2" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.7" From 8f0da6d97f190a21c170f0d2700b7760c66e4932 Mon Sep 17 00:00:00 2001 From: Matan Perelman Date: Sun, 21 May 2023 17:09:22 +0300 Subject: [PATCH 068/234] device_link: Fix windows get_free_disk_space --- pymobiledevice3/services/device_link.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pymobiledevice3/services/device_link.py b/pymobiledevice3/services/device_link.py index 1796cb271..b099a51b1 100644 --- a/pymobiledevice3/services/device_link.py +++ b/pymobiledevice3/services/device_link.py @@ -1,6 +1,5 @@ import ctypes import datetime -import os import shutil import struct from pathlib import Path @@ -130,8 +129,7 @@ def upload_files(self, message): self.status_response(0) def get_free_disk_space(self, message): - vfs = os.statvfs(self.root_path) - freespace = vfs.f_bavail * vfs.f_bsize + freespace = shutil.disk_usage(self.root_path).free self.status_response(0, status_dict=freespace) def move_items(self, message): From 8643b7ea1f05f5f0e6bddd1520ea785b5c140d07 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 21 May 2023 19:47:17 +0300 Subject: [PATCH 069/234] pyproject: bump version to 1.42.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 14d041e85..1599d60eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "1.42.2" +version = "1.42.3" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.7" From 76a50f0338196d45eb1abaf66b97464b361a4512 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 22 May 2023 08:18:19 +0300 Subject: [PATCH 070/234] cli: fix the docstring for `webinspector launch` --- pymobiledevice3/cli/webinspector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/webinspector.py b/pymobiledevice3/cli/webinspector.py index a3941f832..c23f41680 100644 --- a/pymobiledevice3/cli/webinspector.py +++ b/pymobiledevice3/cli/webinspector.py @@ -109,7 +109,7 @@ def opened_tabs(lockdown: LockdownClient, verbose, timeout): @catch_errors def launch(lockdown: LockdownClient, url, timeout): """ - Create a specific URL in Safari. + Launch a specific URL in Safari. \b Opt-in: From b7434af6f82396e0cf2c48c0cf509fba8c615c32 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Mon, 22 May 2023 13:53:50 +0300 Subject: [PATCH 071/234] pcapd: fix `device_packet_struct` for newer iOS versions --- pymobiledevice3/services/pcapd.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/services/pcapd.py b/pymobiledevice3/services/pcapd.py index 7675f5ea2..a05b1329c 100755 --- a/pymobiledevice3/services/pcapd.py +++ b/pymobiledevice3/services/pcapd.py @@ -301,8 +301,8 @@ 'header_version' / Byte, 'packet_length' / Int32ub, 'interface_type' / Byte, - Padding(2), - Padding(1), + 'unit' / Int16ub, + 'io' / Byte, 'protocol_family' / Int32ub, 'frame_pre_length' / Int32ub, 'frame_post_length' / Int32ub, @@ -314,7 +314,8 @@ 'ecomm' / Padded(17, CString('utf8')), 'seconds' / Int32ub, 'microseconds' / Int32ub, - 'data' / Bytes(this.packet_length) + Seek(this.header_length), + 'data' / Bytes(this.packet_length), ) From 2b513ab26ffda3b9cd62e1d647143e0301317c02 Mon Sep 17 00:00:00 2001 From: matan1008 Date: Mon, 22 May 2023 13:54:52 +0300 Subject: [PATCH 072/234] pcap: Split pcap generation logic from cli --- pymobiledevice3/cli/cli_common.py | 9 ++++++ pymobiledevice3/cli/pcap.py | 50 +++++++++++++++++-------------- pymobiledevice3/services/pcapd.py | 7 +++-- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index 9c4a3f929..40aaa6c3c 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -6,6 +6,7 @@ import click import coloredlogs +import hexdump import inquirer from inquirer.themes import GreenPassion from pygments import formatters, highlight, lexers @@ -35,6 +36,14 @@ def print_json(buf, colored=True, default=default_json_encoder): print(formatted_json) +def print_hex(data, colored=True): + hex_dump = hexdump.hexdump(data, result='return') + if colored: + print(highlight(hex_dump, lexers.HexdumpLexer(), formatters.TerminalTrueColorFormatter(style='native'))) + else: + print(hex_dump, end='\n\n') + + def set_verbosity(ctx, param, value): coloredlogs.set_level(logging.INFO - (value * 10)) diff --git a/pymobiledevice3/cli/pcap.py b/pymobiledevice3/cli/pcap.py index d868ea233..e68df3557 100644 --- a/pymobiledevice3/cli/pcap.py +++ b/pymobiledevice3/cli/pcap.py @@ -1,10 +1,10 @@ from datetime import datetime +from typing import IO import click -import hexdump from pygments import formatters, highlight, lexers -from pymobiledevice3.cli.cli_common import Command +from pymobiledevice3.cli.cli_common import Command, print_hex from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.pcapd import PcapdService @@ -15,35 +15,41 @@ def cli(): pass +def print_packet_header(packet, color: bool): + date = datetime.fromtimestamp(packet.seconds + (packet.microseconds / 1000000)) + data = ( + f'{date}: ' + f'Process {packet.comm} ({packet.pid}), ' + f'Interface: {packet.interface_name} ({packet.interface_type.name}), ' + f'Family: {packet.protocol_family.name}' + ) + if not color: + print(data) + else: + print(highlight(data, lexers.HspecLexer(), formatters.TerminalTrueColorFormatter(style='native')), end='') + + +def print_packet(packet, color: bool): + """ Return the packet so it can be chained in a generator """ + print_packet_header(packet, color) + print_hex(packet.data, color) + return packet + + @cli.command(cls=Command) @click.argument('out', type=click.File('wb'), required=False) @click.option('-c', '--count', type=click.INT, default=-1, help='Number of packets to sniff. Omit to endless sniff.') @click.option('--process', default=None, help='Process to filter. Omit for all.') @click.option('--color/--no-color', default=True) -def pcap(lockdown: LockdownClient, out, count, process, color): +def pcap(lockdown: LockdownClient, out: IO, count: int, process: str, color: bool): """ sniff device traffic """ service = PcapdService(lockdown=lockdown) packets_generator = service.watch(packets_count=count, process=process) + if out is not None: - service.write_to_pcap(out, packets_generator) + packets_generator_with_print = map(lambda p: print_packet(p, color), packets_generator) + service.write_to_pcap(out, packets_generator_with_print) return - formatter = formatters.TerminalTrueColorFormatter(style='native') - for packet in packets_generator: - date = datetime.fromtimestamp(packet.seconds + (packet.microseconds / 1000000)) - data = ( - f'{date}: ' - f'Process {packet.comm} ({packet.pid}), ' - f'Interface: {packet.interface_name} ({packet.interface_type.name}), ' - f'Family: {packet.protocol_family.name}' - ) - if not color: - print(data) - else: - print(highlight(data, lexers.HspecLexer(), formatter), end='') - hex_dump = hexdump.hexdump(packet.data, result='return') - if color: - print(hex_dump, end='\n\n') - else: - print(highlight(hex_dump, lexers.HexdumpLexer(), formatter)) + print_packet(packet, color) diff --git a/pymobiledevice3/services/pcapd.py b/pymobiledevice3/services/pcapd.py index a05b1329c..de7b805bf 100755 --- a/pymobiledevice3/services/pcapd.py +++ b/pymobiledevice3/services/pcapd.py @@ -3,8 +3,9 @@ import enum import socket import struct +from typing import Generator -from construct import Byte, Bytes, CString, Int32ub, Int32ul, Padded, Padding, Struct, this +from construct import Byte, Bytes, Container, CString, Int16ub, Int32ub, Int32ul, Padded, Seek, Struct, this from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.base_service import BaseService @@ -321,7 +322,7 @@ class PcapdService(BaseService): """ - Starting iOS 5, apple added a remote virtual interface (RVI) facility that allows mirroring networks trafic from + Starting iOS 5, apple added a remote virtual interface (RVI) facility that allows mirroring networks traffic from an iOS device. On macOS, the virtual interface can be enabled with the rvictl command. This script allows to use this service on other systems. """ @@ -330,7 +331,7 @@ class PcapdService(BaseService): def __init__(self, lockdown: LockdownClient): super().__init__(lockdown, self.SERVICE_NAME) - def watch(self, packets_count: int = -1, process: str = None): + def watch(self, packets_count: int = -1, process: str = None) -> Generator[Container, None, None]: packet_index = 0 while packet_index != packets_count: d = self.service.recv_plist() From 88e4b514e6477c499c9a0139aef2fcbeb1b08d0c Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 24 May 2023 09:15:43 +0300 Subject: [PATCH 073/234] pyproject: bump version to 1.42.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1599d60eb..8cc35af66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "1.42.3" +version = "1.42.4" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.7" From ae31a67799fc9e8610bac8416bfa241b8acfd020 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Tue, 6 Jun 2023 09:55:31 +0300 Subject: [PATCH 074/234] crash_reports: fix `get_new_sysdiagnose()` on 17.0 --- pymobiledevice3/services/crash_reports.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index 528844094..e6ac45444 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -10,6 +10,8 @@ from pymobiledevice3.services.afc import AfcService, AfcShell from pymobiledevice3.services.os_trace import OsTraceService +SYSDIAGNOSE_PROCESS_NAMES = ('sysdiagnose', 'sysdiagnosed') + class CrashReportsManager: COPY_MOBILE_NAME = 'com.apple.crashreportcopymobile' @@ -116,8 +118,8 @@ def get_new_sysdiagnose(self, out: str, erase: bool = True) -> None: sysdiagnose_filename = None for syslog_entry in OsTraceService(lockdown=self.lockdown).syslog(): - if (posixpath.basename(syslog_entry.filename) != 'sysdiagnose') or \ - (posixpath.basename(syslog_entry.image_name) != 'sysdiagnose'): + if (posixpath.basename(syslog_entry.filename) not in SYSDIAGNOSE_PROCESS_NAMES) or \ + (posixpath.basename(syslog_entry.image_name) not in SYSDIAGNOSE_PROCESS_NAMES): # filter only sysdianose lines continue From fde0437f42089aa5da6b762abd2fe141dfdccd5a Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 7 Jun 2023 14:04:50 +0300 Subject: [PATCH 075/234] tss: update client version --- pymobiledevice3/restore/tss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/restore/tss.py b/pymobiledevice3/restore/tss.py index 05713159b..5fbeefea6 100644 --- a/pymobiledevice3/restore/tss.py +++ b/pymobiledevice3/restore/tss.py @@ -12,7 +12,7 @@ TSS_CONTROLLER_ACTION_URL = 'http://gs.apple.com/TSS/controller?action=2' -TSS_CLIENT_VERSION_STRING = 'libauthinstall-914.40.2.0.1' +TSS_CLIENT_VERSION_STRING = 'libauthinstall-973.0.1' logger = logging.getLogger(__name__) From da516c4bc59bcfade3d19a3bf064ea8056c30340 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 7 Jun 2023 14:05:12 +0300 Subject: [PATCH 076/234] tss: fix `apply_restore_request_rules()` typing --- pymobiledevice3/restore/tss.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/restore/tss.py b/pymobiledevice3/restore/tss.py index 5fbeefea6..105493bd8 100644 --- a/pymobiledevice3/restore/tss.py +++ b/pymobiledevice3/restore/tss.py @@ -55,7 +55,8 @@ def __init__(self): } @staticmethod - def apply_restore_request_rules(tss_entry: typing.Mapping, parameters: typing.Mapping, rules: list): + def apply_restore_request_rules(tss_entry: typing.MutableMapping, parameters: typing.MutableMapping, + rules: typing.List): for rule in rules: conditions_fulfilled = True conditions = rule['Conditions'] From 32055954a9571311a48ef36555db311b59c12042 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 7 Jun 2023 14:06:44 +0300 Subject: [PATCH 077/234] mobile_image_mounter: support Personalized image for iOS 17.0 --- pymobiledevice3/__main__.py | 1 + pymobiledevice3/cli/mounter.py | 118 ++++++----- pymobiledevice3/exceptions.py | 5 + .../services/mobile_image_mounter.py | 186 ++++++++++++++---- .../services/test_dvt_secure_socket_proxy.py | 9 +- 5 files changed, 227 insertions(+), 92 deletions(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index 75e762985..c3aa7a4c1 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -77,6 +77,7 @@ def cli(): logger.error('Failed to connect to usbmuxd socket. Make sure it\'s running.') except MessageNotSupportedError: logger.error('Message not supported for this iOS version') + traceback.print_exc() except InternalError: logger.error('Internal Error') except DeveloperModeIsNotEnabledError: diff --git a/pymobiledevice3/cli/mounter.py b/pymobiledevice3/cli/mounter.py index 6b861650a..ccd222c58 100644 --- a/pymobiledevice3/cli/mounter.py +++ b/pymobiledevice3/cli/mounter.py @@ -1,8 +1,8 @@ import json import logging -import plistlib +from functools import update_wrapper from pathlib import Path -from typing import IO, List +from typing import List from urllib.error import URLError from urllib.request import urlopen @@ -12,9 +12,10 @@ from pymobiledevice3.cli.cli_common import Command, print_json from pymobiledevice3.common import get_home_folder -from pymobiledevice3.exceptions import NotMountedError, UnsupportedCommandError +from pymobiledevice3.exceptions import AlreadyMountedError, NotMountedError, UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.mobile_image_mounter import MobileImageMounterService +from pymobiledevice3.services.mobile_image_mounter import DeveloperDiskImageMounter, MobileImageMounterService, \ + PersonalizedImageMounter DISK_IMAGE_TREE = 'https://api.github.com/repos/pdso/DeveloperDiskImage/git/trees/master' DEVELOPER_DISK_IMAGE_URL = 'https://github.com/pdso/DeveloperDiskImage/raw/master/{ios_version}/{file_name}' @@ -22,6 +23,18 @@ logger = logging.getLogger(__name__) +def catch_errors(func): + def catch_function(*args, **kwargs): + try: + return func(*args, **kwargs) + except AlreadyMountedError: + logger.error('Given image was already mounted') + except UnsupportedCommandError: + logger.error('Your iOS version doesn\'t support this command') + + return update_wrapper(catch_function, func) + + @click.group() def cli(): """ mounter cli """ @@ -62,19 +75,26 @@ def mounter_lookup(lockdown: LockdownClient, color, image_type): logger.error(f'Disk image of type: {image_type} is not mounted') -@mounter.command('umount', cls=Command) -@click.option('-t', '--image-type', type=click.Choice(['Developer', 'Cryptex']), default='Developer') -@click.option('-p', '--mount-path', help='Only needed for older iOS version', default='/Developer') -def mounter_umount(lockdown: LockdownClient, image_type: str, mount_path: str): - """ unmount developer image. """ - image_mounter = MobileImageMounterService(lockdown=lockdown) +@mounter.command('umount-developer', cls=Command) +@catch_errors +def mounter_umount_developer(lockdown: LockdownClient): + """ unmount Developer image """ + try: + DeveloperDiskImageMounter(lockdown=lockdown).umount() + logger.info('Developer image unmounted successfully') + except NotMountedError: + logger.error('Developer image isn\'t currently mounted') + + +@mounter.command('umount-personalized', cls=Command) +@catch_errors +def mounter_umount_personalized(lockdown: LockdownClient): + """ unmount Personalized image """ try: - image_mounter.umount(mount_path, image_type=image_type, signature=b'') - logger.info('DeveloperDiskImage unmounted successfully') + PersonalizedImageMounter(lockdown=lockdown).umount() + logger.info('Personalized image unmounted successfully') except NotMountedError: - logger.error('DeveloperDiskImage isn\'t currently mounted') - except UnsupportedCommandError: - logger.error('Your iOS version doesn\'t support this command') + logger.error('Personalized image isn\'t currently mounted') def download_file(url, local_filename): @@ -98,34 +118,25 @@ def get_all_versions() -> List[str]: return [item.get('path') for item in json_data.get('tree')][0:-3] -@mounter.command('mount', cls=Command) -@click.argument('image-path', type=click.Path(exists=True)) -@click.argument('signature', type=click.Path(exists=True)) -@click.argument('image-type', type=click.Choice(['Developer', 'Cryptex']), default='Developer') -@click.option('--trust-cache', type=click.File('rb'), help='Used only for Cryptex images') -@click.option('--info-plist', type=click.File('rb'), help='Used only for Cryptex images') -def mounter_mount(lockdown: LockdownClient, image_path: str, signature: str, image_type: str, trust_cache: IO = None, - info_plist: IO = None): - """ mount developer image. """ - image_mounter = MobileImageMounterService(lockdown=lockdown) - if image_mounter.is_image_mounted(image_type): - logger.error(f'{image_type} is already mounted') - return +@mounter.command('mount-developer', cls=Command) +@click.argument('image', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.argument('signature', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@catch_errors +def mounter_mount_developer(lockdown: LockdownClient, image: str, signature: str): + """ mount developer image """ + DeveloperDiskImageMounter(lockdown=lockdown).mount(Path(image), Path(signature)) + logger.info('Developer image mounted successfully') - if trust_cache is not None: - trust_cache = trust_cache.read() - if info_plist is not None: - info_plist = plistlib.load(info_plist) - - image_path = Path(image_path) - signature = Path(signature) - image_path = image_path.read_bytes() - signature = signature.read_bytes() - - image_mounter.upload_image(image_type, image_path, signature) - image_mounter.mount(image_type, signature, trust_cache=trust_cache, info_plist=info_plist) - logger.info(f'{image_type} mounted successfully') +@mounter.command('mount-personalized', cls=Command) +@click.argument('image', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.argument('trust-cache', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.argument('build-manifest', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@catch_errors +def mounter_mount_personalized(lockdown: LockdownClient, image: str, trust_cache: str, build_manifest: str): + """ mount personalized image """ + PersonalizedImageMounter(lockdown=lockdown).mount(Path(image), Path(build_manifest), Path(trust_cache)) + logger.info('Personalized image mounted successfully') @mounter.command('auto-mount', cls=Command) @@ -144,7 +155,7 @@ def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): xcode = get_home_folder() / 'Xcode.app' xcode.mkdir(parents=True, exist_ok=True) - image_mounter = MobileImageMounterService(lockdown=lockdown) + image_mounter = DeveloperDiskImageMounter(lockdown=lockdown) if image_mounter.is_image_mounted(image_type): logger.error('DeveloperDiskImage is already mounted') return @@ -185,11 +196,7 @@ def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): f'Please make sure your user has the necessary permissions') return - image_path = image_path.read_bytes() - signature = signature.read_bytes() - - image_mounter.upload_image(image_type, image_path, signature) - image_mounter.mount(image_type, signature) + image_mounter.mount(image_path, signature) logger.info('DeveloperDiskImage mounted successfully') @@ -201,10 +208,11 @@ def mounter_query_developer_mode_status(lockdown: LockdownClient, color): @mounter.command('query-nonce', cls=Command) +@click.option('--image-type') @click.option('--color/--no-color', default=True) -def mounter_query_nonce(lockdown: LockdownClient, color): +def mounter_query_nonce(lockdown: LockdownClient, image_type: str, color: bool): """ Query nonce """ - print_json(MobileImageMounterService(lockdown=lockdown).query_nonce(), colored=color) + print_json(MobileImageMounterService(lockdown=lockdown).query_nonce(image_type), colored=color) @mounter.command('query-personalization-identifiers', cls=Command) @@ -214,13 +222,23 @@ def mounter_query_personalization_identifiers(lockdown: LockdownClient, color): print_json(MobileImageMounterService(lockdown=lockdown).query_personalization_identifiers(), colored=color) +@mounter.command('query-personalization-manifest', cls=Command) +@click.option('--color/--no-color', default=True) +def mounter_query_personalization_manifest(lockdown: LockdownClient, color): + """ Query personalization manifest """ + result = [] + mounter = MobileImageMounterService(lockdown=lockdown) + for device in mounter.copy_devices(): + result.append(mounter.query_personalization_manifest(device['PersonalizedImageType'], device['ImageSignature'])) + print_json(result, colored=color) + + @mounter.command('roll-personalization-nonce', cls=Command) def mounter_roll_personalization_nonce(lockdown: LockdownClient): MobileImageMounterService(lockdown=lockdown).roll_personalization_nonce() @mounter.command('roll-cryptex-nonce', cls=Command) -@click.option('--color/--no-color', default=True) def mounter_roll_cryptex_nonce(lockdown: LockdownClient): """ Roll cryptex nonce (will reboot) """ MobileImageMounterService(lockdown=lockdown).roll_cryptex_nonce() diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index 51f809f0b..ab227b7eb 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -121,6 +121,11 @@ class AlreadyMountedError(PyMobileDevice3Exception): pass +class MissingManifestError(PyMobileDevice3Exception): + """ No manifest could be found """ + pass + + class UnsupportedCommandError(PyMobileDevice3Exception): """ Given command isn't supported for this iOS version """ pass diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index f48a7f56d..e25785a82 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -1,12 +1,18 @@ +import hashlib +import plistlib +from pathlib import Path from typing import List, Mapping from pymobiledevice3.exceptions import AlreadyMountedError, DeveloperModeIsNotEnabledError, InternalError, \ - MessageNotSupportedError, NotMountedError, PyMobileDevice3Exception, UnsupportedCommandError + MessageNotSupportedError, MissingManifestError, NotMountedError, PyMobileDevice3Exception, \ + UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.restore.tss import TSSRequest from pymobiledevice3.services.base_service import BaseService class MobileImageMounterService(BaseService): + # implemented in /usr/libexec/mobile_storage_proxy SERVICE_NAME = 'com.apple.mobile.mobile_image_mounter' def __init__(self, lockdown: LockdownClient): @@ -41,28 +47,23 @@ def is_image_mounted(self, image_type: str) -> bool: except NotMountedError: return False - def umount(self, mount_path: str, image_type: str = None, signature: bytes = None) -> None: - """ umount image. """ + def unmount_image(self, mount_path: str) -> None: + """ umount image (Added on iOS 14.0) """ request = {'Command': 'UnmountImage', 'MountPath': mount_path} - - if image_type is not None: - request['ImageType'] = image_type - - if signature is not None: - request['ImageSignature'] = signature - response = self.service.send_recv_plist(request) error = response.get('Error') if error: if error == 'UnknownCommand': raise UnsupportedCommandError() + elif 'There is no matching entry' in response.get('DetailedError', ''): + raise NotMountedError(response) elif error == 'InternalError': - raise InternalError() + raise InternalError(response) else: - raise NotMountedError() + raise PyMobileDevice3Exception(response) - def mount(self, image_type: str, signature: bytes, trust_cache: bytes = None, info_plist: Mapping = None) -> None: + def mount_image(self, image_type: str, signature: bytes, extras: Mapping = None) -> None: """ Upload image into device. """ if self.is_image_mounted(image_type): @@ -72,16 +73,8 @@ def mount(self, image_type: str, signature: bytes, trust_cache: bytes = None, in 'ImageType': image_type, 'ImageSignature': signature} - if image_type == 'Cryptex': - if trust_cache is None: - raise ValueError('Cryptex image requires a ImageTrustCache to be also supplied') - - if info_plist is None: - raise ValueError('Cryptex image requires a ImageInfoPlist to be also supplied') - - request['ImageTrustCache'] = trust_cache - request['ImageInfoPlist'] = info_plist - + if extras is not None: + request.update(extras) response = self.service.send_recv_plist(request) if 'Developer mode is not enabled' in response.get('DetailedError', ''): @@ -121,15 +114,17 @@ def query_developer_mode_status(self) -> bool: except KeyError as e: raise MessageNotSupportedError from e - def query_nonce(self) -> bytes: - response = self.service.send_recv_plist({'Command': 'QueryNonce'}) - + def query_nonce(self, personalized_image_type: str = None) -> bytes: + request = {'Command': 'QueryNonce'} + if personalized_image_type is not None: + request['PersonalizedImageType'] = personalized_image_type + response = self.service.send_recv_plist(request) try: return response['PersonalizationNonce'] except KeyError as e: raise MessageNotSupportedError from e - def query_personalization_identifiers(self, image_type: str = None) -> bytes: + def query_personalization_identifiers(self, image_type: str = None) -> Mapping: request = {'Command': 'QueryPersonalizationIdentifiers'} if image_type is not None: @@ -142,17 +137,17 @@ def query_personalization_identifiers(self, image_type: str = None) -> bytes: except KeyError as e: raise MessageNotSupportedError from e - def query_personalization_manifest(self, image_type: str, signature: bytes) -> Mapping: - request = {'Command': 'QueryPersonalizationManifest', 'PersonalizedImageType': image_type, - 'ImageType': image_type, 'ImageSignature': signature} - - response = self.service.send_recv_plist(request) - + def query_personalization_manifest(self, image_type: str, signature: bytes) -> bytes: + response = self.service.send_recv_plist({ + 'Command': 'QueryPersonalizationManifest', 'PersonalizedImageType': image_type, 'ImageType': image_type, + 'ImageSignature': signature}) try: - # The response is returned as "ImageSignature" which is wrong, but that's what Apple does + # The response "ImageSignature" is actually an IM4M return response['ImageSignature'] - except KeyError as e: - raise MessageNotSupportedError from e + except KeyError: + if response.get('Error') == 'InternalError': + raise InternalError(response) + raise MissingManifestError() def roll_personalization_nonce(self) -> None: try: @@ -165,3 +160,122 @@ def roll_cryptex_nonce(self) -> None: self.service.send_recv_plist({'Command': 'RollCryptexNonce'}) except ConnectionAbortedError: return + + +class DeveloperDiskImageMounter(MobileImageMounterService): + IMAGE_TYPE = 'Developer' + + def mount(self, image: Path, signature: Path) -> None: + if self.is_image_mounted(self.IMAGE_TYPE): + raise AlreadyMountedError() + + image = Path(image).read_bytes() + signature = Path(signature).read_bytes() + self.upload_image(self.IMAGE_TYPE, image, signature) + self.mount_image(self.IMAGE_TYPE, signature) + + def umount(self) -> None: + self.unmount_image('/Developer') + + +class PersonalizedImageMounter(MobileImageMounterService): + IMAGE_TYPE = 'Personalized' + + def mount(self, image: Path, build_manifest: Path, trust_cache: Path, + info_plist: Mapping = None) -> None: + if self.is_image_mounted(self.IMAGE_TYPE): + raise AlreadyMountedError() + + image = image.read_bytes() + trust_cache = trust_cache.read_bytes() + + # try to fetch the personalization manifest if the device already has one + # in case of failure, the service will close the socket, so we'll have to reestablish the connection + # and query the manifest from Apple's ticket server instead + try: + manifest = self.query_personalization_manifest('DeveloperDiskImage', hashlib.sha384(image).digest()) + except MissingManifestError: + self.service = self.lockdown.start_service(self.SERVICE_NAME) + manifest = self.get_manifest_from_tss(plistlib.loads(build_manifest.read_bytes())) + + self.upload_image(self.IMAGE_TYPE, image, manifest) + + extras = {} + if info_plist is not None: + extras['ImageInfoPlist'] = info_plist + extras['ImageTrustCache'] = trust_cache + self.mount_image(self.IMAGE_TYPE, manifest, extras=extras) + + def umount(self) -> None: + self.unmount_image('/System/Developer') + + def get_manifest_from_tss(self, build_manifest: Mapping) -> bytes: + request = TSSRequest() + + personalization_identifiers = self.query_personalization_identifiers() + for key, value in personalization_identifiers.items(): + if key.startswith('Ap,'): + request.update({key: value}) + + board_id = personalization_identifiers['BoardId'] + chip_id = personalization_identifiers['ChipID'] + + build_identity = None + for tmp_build_identity in build_manifest['BuildIdentities']: + if int(tmp_build_identity['ApBoardID'], 0) == board_id and \ + int(tmp_build_identity['ApChipID'], 0) == chip_id: + build_identity = tmp_build_identity + break + manifest = build_identity['Manifest'] + + parameters = { + 'ApProductionMode': True, + 'ApSecurityDomain': 1, + 'ApSecurityMode': True, + 'ApSupportsImg4': True, + } + + request.update({ + '@ApImg4Ticket': True, + '@BBTicket': True, + 'ApBoardID': board_id, + 'ApChipID': chip_id, + 'ApECID': self.lockdown.ecid, + 'ApNonce': self.query_nonce('DeveloperDiskImage'), + 'ApProductionMode': True, + 'ApSecurityDomain': 1, + 'ApSecurityMode': True, + 'SepNonce': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + 'UID_MODE': False, + }) + + for key, manifest_entry in manifest.items(): + info_dict = manifest_entry.get('Info') + if info_dict is None: + continue + + if not manifest_entry.get('Trusted', False): + self.logger.debug(f'skipping {key} as it is not trusted') + continue + + # copy this entry + tss_entry = dict(manifest_entry) + + # remove obsolete Info node + tss_entry.pop('Info') + + # handle RestoreRequestRules + if 'RestoreRequestRules' in manifest['LoadableTrustCache']['Info']: + rules = manifest['LoadableTrustCache']['Info']['RestoreRequestRules'] + if rules: + self.logger.debug(f'Applying restore request rules for entry {key}') + tss_entry = request.apply_restore_request_rules(tss_entry, parameters, rules) + + # Make sure we have a Digest key for Trusted items even if empty + if manifest_entry.get('Digest') is None: + tss_entry['Digest'] = bytes() + + request.update({key: tss_entry}) + + response = request.send_receive() + return response['ApImg4Ticket'] diff --git a/tests/services/test_dvt_secure_socket_proxy.py b/tests/services/test_dvt_secure_socket_proxy.py index f9adf3c33..21814c2ab 100644 --- a/tests/services/test_dvt_secure_socket_proxy.py +++ b/tests/services/test_dvt_secure_socket_proxy.py @@ -8,7 +8,7 @@ from pymobiledevice3.services.dvt.instruments.application_listing import ApplicationListing from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo from pymobiledevice3.services.dvt.instruments.process_control import ProcessControl -from pymobiledevice3.services.mobile_image_mounter import MobileImageMounterService +from pymobiledevice3.services.mobile_image_mounter import DeveloperDiskImageMounter DEVICE_SUPPORT = Path('/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport') IMAGE_TYPE = 'Developer' @@ -17,15 +17,12 @@ @pytest.fixture(scope='module', autouse=True) def mount_developer_disk_image(): with LockdownClient() as lockdown: - with MobileImageMounterService(lockdown=lockdown) as mounter: + with DeveloperDiskImageMounter(lockdown=lockdown) as mounter: if mounter.is_image_mounted('Developer'): yield - image_path = DEVICE_SUPPORT / mounter.lockdown.sanitized_ios_version / 'DeveloperDiskImage.dmg' - signature = image_path.with_suffix('.dmg.signature').read_bytes() - mounter.upload_image('Developer', image_path.read_bytes(), signature) try: - mounter.mount(IMAGE_TYPE, signature) + mounter.mount(image_path, image_path.with_suffix('.dmg.signature')) except AlreadyMountedError: pass From 0bc57157b8a13e77afc9ef975200886894670f52 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 09:22:38 +0300 Subject: [PATCH 078/234] test_backup2: handle device connection aborts during password change --- tests/services/test_backup2.py | 44 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/tests/services/test_backup2.py b/tests/services/test_backup2.py index 80fed1a78..13e3ec41f 100644 --- a/tests/services/test_backup2.py +++ b/tests/services/test_backup2.py @@ -1,30 +1,48 @@ import time +from pathlib import Path from ssl import SSLEOFError +from typing import Callable +from pymobiledevice3.exceptions import ConnectionFailedError +from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service PASSWORD = '1234' +def ignore_connection_errors(f: Callable): + """ + The device may become unresponsive for a short while after changing the password settings and reject + incoming connections at different stages + """ + def _wrapper(*args, **kwargs): + while True: + try: + f(*args, **kwargs) + break + except (SSLEOFError, ConnectionAbortedError, OSError, ConnectionFailedError): + time.sleep(1) + + return _wrapper + + +@ignore_connection_errors def change_password(lockdown, old: str = '', new: str = '') -> None: - while True: - try: - with Mobilebackup2Service(lockdown) as service: - service.change_password(old=old, new=new) - except (SSLEOFError, ConnectionAbortedError): - # after large backups, the device requires time to recover - time.sleep(1) - else: - break + with Mobilebackup2Service(lockdown) as service: + service.change_password(old=old, new=new) -def test_backup(lockdown, tmp_path): +@ignore_connection_errors +def backup(lockdown: LockdownClient, backup_directory: Path) -> None: with Mobilebackup2Service(lockdown) as service: - service.backup(full=True, backup_directory=tmp_path) + service.backup(full=True, backup_directory=backup_directory) + + +def test_backup(lockdown, tmp_path): + backup(lockdown, tmp_path) def test_encrypted_backup(lockdown, tmp_path): change_password(lockdown, new=PASSWORD) - with Mobilebackup2Service(lockdown) as service: - service.backup(full=True, backup_directory=tmp_path) + backup(lockdown, tmp_path) change_password(lockdown, old=PASSWORD) From bc3245ae8335f9a6798266edafe6dbbe0a67b599 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 09:49:44 +0300 Subject: [PATCH 079/234] mobile_image_mounter: fix manifest error handling --- pymobiledevice3/services/mobile_image_mounter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index e25785a82..b1cab5876 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -145,8 +145,6 @@ def query_personalization_manifest(self, image_type: str, signature: bytes) -> b # The response "ImageSignature" is actually an IM4M return response['ImageSignature'] except KeyError: - if response.get('Error') == 'InternalError': - raise InternalError(response) raise MissingManifestError() def roll_personalization_nonce(self) -> None: From 208b9df786a6da4d3bdc35bb5cfa536f5993580e Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 10:02:50 +0300 Subject: [PATCH 080/234] restore: fix hadline of missing preflight_info from lockdown --- pymobiledevice3/restore/device.py | 6 +++++- pymobiledevice3/restore/recovery.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/restore/device.py b/pymobiledevice3/restore/device.py index 5e8779414..9de3214a6 100644 --- a/pymobiledevice3/restore/device.py +++ b/pymobiledevice3/restore/device.py @@ -1,5 +1,8 @@ +from contextlib import suppress + from cached_property import cached_property +from pymobiledevice3.exceptions import MissingValueError from pymobiledevice3.irecv import IRecv from pymobiledevice3.lockdown import LockdownClient @@ -42,5 +45,6 @@ def sep_nonce(self): @cached_property def preflight_info(self): if self.lockdown: - return self.lockdown.preflight_info + with suppress(MissingValueError): + return self.lockdown.preflight_info return None diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index 5bceaaa0b..62e6444fc 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -88,7 +88,7 @@ def get_tss_response(self): # normal mode; request baseband ticket as well if self.device.lockdown is not None: - pinfo = self.device.lockdown.preflight_info + pinfo = self.device.preflight_info if pinfo: self.logger.debug('adding preflight info') From 405e60c1422fd24498fc7c03736b245c35e0bd66 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 11:16:12 +0300 Subject: [PATCH 081/234] lockdown: add `wraps()` to `reconnect_on_remote_close()` --- pymobiledevice3/lockdown.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 1680b74c0..83955d33c 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -10,6 +10,7 @@ import uuid from contextlib import contextmanager, suppress from enum import Enum +from functools import wraps from pathlib import Path from typing import Mapping, Optional, Union @@ -42,6 +43,7 @@ def reconnect_on_remote_close(f): transmitted). When this happens, we'll attempt to reconnect. """ + @wraps(f) def _reconnect_on_remote_close(*args, **kwargs): try: return f(*args, **kwargs) From ee5821e20fdaea2fc49416d7d89b67d361c48cfc Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 11:20:39 +0300 Subject: [PATCH 082/234] usbmux: refactor `MuxConnection` into a context-manager --- pymobiledevice3/usbmux.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pymobiledevice3/usbmux.py b/pymobiledevice3/usbmux.py index 60b96fb34..475b88dca 100644 --- a/pymobiledevice3/usbmux.py +++ b/pymobiledevice3/usbmux.py @@ -225,6 +225,12 @@ def _raise_mux_exception(self, result: int, message: str = None): exception = exceptions.get(result, MuxException) raise exception(message) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + class BinaryMuxConnection(MuxConnection): """ old binary protocol """ From b6a9c8fd7830a05717d660c2a06bd28024deb12c Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 11:21:09 +0300 Subject: [PATCH 083/234] service_connection: use `socket.create_connection()` for ipv6 support --- pymobiledevice3/service_connection.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/service_connection.py b/pymobiledevice3/service_connection.py index c12d39879..3d9afc76f 100755 --- a/pymobiledevice3/service_connection.py +++ b/pymobiledevice3/service_connection.py @@ -62,7 +62,7 @@ class Medium(Enum): USBMUX = auto() -class ServiceConnection(object): +class ServiceConnection: """ wrapper for usbmux tcp-relay connections """ def __init__(self, sock: socket.socket, mux_device: MuxDevice = None): @@ -77,8 +77,7 @@ def __init__(self, sock: socket.socket, mux_device: MuxDevice = None): @staticmethod def create_using_tcp(hostname: str, port: int) -> 'ServiceConnection': - sock = socket.socket() - sock.connect((hostname, port)) + sock = socket.create_connection((hostname, port)) return ServiceConnection(sock) @staticmethod From 1e63506ba64d3b1b1fc316b0264b02a0e4a8a35f Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 14 Jun 2023 11:21:41 +0300 Subject: [PATCH 084/234] lockdown: refactor `LockdownClient` instance creation --- pymobiledevice3/bonjour.py | 6 +- pymobiledevice3/cli/cli_common.py | 10 +- pymobiledevice3/cli/usbmux.py | 4 +- pymobiledevice3/lockdown.py | 435 +++++++++--------- pymobiledevice3/pair_records.py | 81 ++++ tests/services/conftest.py | 4 +- tests/services/test_crash_reports.py | 4 +- .../services/test_dvt_secure_socket_proxy.py | 4 +- tests/services/test_tcp_forwarder.py | 4 +- 9 files changed, 329 insertions(+), 223 deletions(-) create mode 100644 pymobiledevice3/pair_records.py diff --git a/pymobiledevice3/bonjour.py b/pymobiledevice3/bonjour.py index 250c83a6d..9af5fbf66 100644 --- a/pymobiledevice3/bonjour.py +++ b/pymobiledevice3/bonjour.py @@ -6,7 +6,7 @@ from zeroconf import InterfaceChoice, InterfacesType, IPVersion, ServiceBrowser, ServiceListener, Zeroconf -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import LockdownClient, create_using_tcp SERVICE_NAME = '_apple-mobdev2._tcp.local.' @@ -52,10 +52,10 @@ def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: ipv6 = [socket.inet_ntop(socket.AF_INET6, address) for address in info.addresses_by_version(IPVersion.V6Only)] try: - lockdown = LockdownClient(hostname=ipv4[0], autopair=False) + lockdown = create_using_tcp(hostname=ipv4[0], autopair=False) for pair_record in self.pair_records: - lockdown = LockdownClient(hostname=ipv4[0], autopair=False, pair_record=pair_record) + lockdown = create_using_tcp(hostname=ipv4[0], autopair=False, pair_record=pair_record) if lockdown.paired: break except ConnectionRefusedError: diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index 40aaa6c3c..3e4179c51 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -12,7 +12,7 @@ from pygments import formatters, highlight, lexers from pymobiledevice3.exceptions import NoDeviceSelectedError -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux from pymobiledevice3.usbmux import select_devices_by_connection_type @@ -86,15 +86,15 @@ def udid(ctx, param, value): return if value is not None: - return LockdownClient(serial=value) + return create_using_usbmux(serial=value) devices = select_devices_by_connection_type(connection_type='USB') if len(devices) <= 1: - return LockdownClient() + return create_using_usbmux() devices_options = [] for device in devices: - lockdown_client = LockdownClient(serial=device.serial) + lockdown_client = create_using_usbmux(serial=device.serial) device_info = DeviceInfo(lockdown_client) devices_options.append(device_info) @@ -112,7 +112,7 @@ def udid(ctx, param, value): if '_PYMOBILEDEVICE3_COMPLETE' in os.environ: # prevent lockdown connection establishment when in autocomplete mode return - return LockdownClient(serial=value, autopair=False) + return create_using_usbmux(serial=value, autopair=False) class BasedIntParamType(click.ParamType): diff --git a/pymobiledevice3/cli/usbmux.py b/pymobiledevice3/cli/usbmux.py index 0f0d3a6b9..11a680325 100644 --- a/pymobiledevice3/cli/usbmux.py +++ b/pymobiledevice3/cli/usbmux.py @@ -5,7 +5,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.cli.cli_common import print_json -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.tcp_forwarder import TcpForwarder logger = logging.getLogger(__name__) @@ -61,7 +61,7 @@ def usbmux_list(color, usb, network): if network and not device.is_network: continue - lockdown = LockdownClient(udid, autopair=False, usbmux_connection_type=device.connection_type) + lockdown = create_using_usbmux(udid, autopair=False, connection_type=device.connection_type) connected_devices.append(lockdown.short_info) print_json(connected_devices, colored=color) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 83955d33c..a4b27f684 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -2,67 +2,32 @@ import datetime import logging import os -import platform import plistlib -import sys import tempfile import time -import uuid +from abc import ABC, abstractmethod from contextlib import contextmanager, suppress from enum import Enum from functools import wraps from pathlib import Path -from typing import Mapping, Optional, Union +from typing import Dict, Mapping from packaging.version import Version from pymobiledevice3 import usbmux from pymobiledevice3.ca import ca_do_everything -from pymobiledevice3.common import get_home_folder from pymobiledevice3.exceptions import CannotStopSessionError, ConnectionTerminatedError, FatalPairingError, \ IncorrectModeError, InvalidHostIDError, InvalidServiceError, LockdownError, MissingValueError, NotPairedError, \ PairingDialogResponsePendingError, PairingError, PasswordRequiredError, SetProhibitedError, StartServiceError, \ UserDeniedPairingError from pymobiledevice3.irecv_devices import IRECV_DEVICES -from pymobiledevice3.service_connection import Medium, ServiceConnection +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.usbmux import PlistMuxConnection from pymobiledevice3.utils import sanitize_ios_version SYSTEM_BUID = '30142955-444094379208051516' - -LOCKDOWN_PATH = { - 'win32': Path(os.environ.get('ALLUSERSPROFILE', ''), 'Apple', 'Lockdown'), - 'darwin': Path('/var/db/lockdown/'), - 'linux': Path('/var/lib/lockdown/'), -} - - -def reconnect_on_remote_close(f): - """ - lockdownd's _socket_select will close the connection after 60 seconds of "radio-silent" (no data has been - transmitted). When this happens, we'll attempt to reconnect. - """ - - @wraps(f) - def _reconnect_on_remote_close(*args, **kwargs): - try: - return f(*args, **kwargs) - except (BrokenPipeError, ConnectionTerminatedError): - self = args[0] - - # first we release the socket on our end to avoid a ResourceWarning - self.close() - - # now we re-establish the connection - self.logger.debug('remote device closed the connection. reconnecting...') - self.service = ServiceConnection.create(self.medium, self.identifier, self.SERVICE_PORT, - connection_type=self.usbmux_connection_type) - self.validate_pairing() - return f(*args, **kwargs) - - return _reconnect_on_remote_close - - DOMAINS = ['com.apple.disk_usage', 'com.apple.disk_usage.factory', 'com.apple.mobile.battery', @@ -97,6 +62,9 @@ def _reconnect_on_remote_close(*args, **kwargs): 'com.apple.fmip', 'com.apple.Accessibility', ] +DEFAULT_LABEL = 'pymobiledevice3' +SERVICE_PORT = 62078 + class DeviceClass(Enum): IPHONE = 'iPhone' @@ -107,50 +75,58 @@ class DeviceClass(Enum): UNKNOWN = 'Unknown' -class LockdownClient(object): - DEFAULT_CLIENT_NAME = 'pymobiledevice3' - SERVICE_PORT = 62078 +def _reconnect_on_remote_close(f): + """ + lockdownd's _socket_select will close the connection after 60 seconds of "radio-silent" (no data has been + transmitted). When this happens, we'll attempt to reconnect. + """ + + @wraps(f) + def _inner_reconnect_on_remote_close(*args, **kwargs): + try: + return f(*args, **kwargs) + except (BrokenPipeError, ConnectionTerminatedError): + self = args[0] + + # first we release the socket on our end to avoid a ResourceWarning + self.close() - def __init__(self, serial: str = None, hostname: str = None, client_name: str = DEFAULT_CLIENT_NAME, - autopair: bool = True, usbmux_connection_type: str = None, pair_timeout: int = None, - local_hostname: str = None, pair_record: Mapping = None, - pairing_records_cache_folder: Path = None): - """ - :param serial: serial number for device to connect to (over usbmuxd) - :param hostname: connect to given hostname using TCP instead of usbmuxd - :param client_name: user agent to use when identifying for lockdownd - :param autopair: should automatically attempt pairing with device - :param usbmux_connection_type: can be either "USB" or "Network" to specify what connection type to use - :param pair_timeout: if autopair, use this timeout for user's Trust dialog. If None, will wait forever - :param local_hostname: use given hostname to generate the HostID inside the pair record - :param pair_record: use this pair record instead of the one already stored - """ - self.identifier = None - self.usbmux_connection_type = usbmux_connection_type + # now we re-establish the connection + self.logger.debug('remote device closed the connection. reconnecting...') + self.service = self._create_service_connection(self.port) + self.validate_pairing() + return f(*args, **kwargs) - if hostname is not None: - self.medium = Medium.TCP - self.identifier = hostname - else: - self.medium = Medium.USBMUX - self.identifier = serial + return _inner_reconnect_on_remote_close - self.service = ServiceConnection.create(self.medium, self.identifier, self.SERVICE_PORT, - connection_type=self.usbmux_connection_type) +class LockdownClient(ABC): + 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): + """ + Create a LockdownClient instance + + :param service: lockdownd connection handler + :param host_id: Used as the host identifier for the handshake + :param identifier: Used as an identifier to look for the device pair record + :param label: lockdownd user-agent + :param system_buid: System's unique identifier + :param pair_record: Use this pair record instead of the default behavior (search in host/create our own) + :param pairing_records_cache_folder: Use the following location to search and save pair records + :param port: lockdownd service port + """ self.logger = logging.getLogger(__name__) + self.service = service + self.identifier = identifier + self.label = label + self.host_id = host_id + self.system_buid = system_buid + self.pair_record = pair_record self.paired = False self.session_id = None - self.host_id = self.generate_host_id(local_hostname) - self.system_buid = self.get_system_buid() - self.label = client_name - self.pair_record = pair_record - - if pairing_records_cache_folder is None: - self.pairing_records_cache_folder = get_home_folder() - else: - self.pairing_records_cache_folder = pairing_records_cache_folder - self.pairing_records_cache_folder.mkdir(parents=True, exist_ok=True) + self.pairing_records_cache_folder = pairing_records_cache_folder + self.port = port if self.query_type() != 'com.apple.mobile.lockdown': raise IncorrectModeError() @@ -162,30 +138,40 @@ def __init__(self, serial: str = None, hostname: str = None, client_name: str = self.product_version = self.all_values.get('ProductVersion') self.product_type = self.all_values.get('ProductType') - if self.identifier is None and self.medium == Medium.USBMUX: - # attempt get identifier from mux device serial - self.identifier = self.service.mux_device.serial - - if self.identifier is None and self.udid is not None: - # attempt get identifier from queried udid - self.identifier = self.udid - - if not self.validate_pairing(): - # device is not paired + @classmethod + def create(cls, service: ServiceConnection, identifier: str = None, system_buid: str = SYSTEM_BUID, + label: str = DEFAULT_LABEL, autopair: bool = True, pair_timeout: int = None, local_hostname: str = None, + pair_record: Mapping = None, pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT, + **cls_specific_args): + """ + Create a LockdownClient instance + + :param service: lockdownd connection handler + :param identifier: Used as an identifier to look for the device pair record + :param system_buid: System's unique identifier + :param label: lockdownd user-agent + :param autopair: Attempt to pair with device (blocking) if not already paired + :param pair_timeout: Timeout for autopair + :param local_hostname: Used as a seed to generate the HostID + :param pair_record: Use this pair record instead of the default behavior (search in host/create our own) + :param pairing_records_cache_folder: Use the following location to search and save pair records + :param port: lockdownd service port + :param cls_specific_args: Additional members to pass into LockdownClient subclasses + :return: LockdownClient subclass + """ + host_id = generate_host_id(local_hostname) - if not autopair: - # but pairing by default was not requested - return + pairing_records_cache_folder = create_pairing_records_cache_folder(pairing_records_cache_folder) + if pair_record is None: + pair_record = get_preferred_pair_record(identifier, pairing_records_cache_folder) - self.pair(timeout=pair_timeout) + lockdown_client = cls( + service, host_id=host_id, identifier=identifier, label=label, system_buid=system_buid, + pair_record=pair_record, pairing_records_cache_folder=pairing_records_cache_folder, port=port, + **cls_specific_args) - # get session_id - if not self.validate_pairing(): - raise FatalPairingError() - - # reload data after pairing - self.all_values = self.get_value() - self.udid = self.all_values.get('UniqueDeviceID') + lockdown_client._handle_autopair(autopair, pair_timeout) + return lockdown_client def __repr__(self) -> str: return f'<{self.__class__.__name__} ID:{self.identifier} VERSION:{self.product_version} ' \ @@ -197,9 +183,6 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close() - def query_type(self) -> str: - return self._request('QueryType').get('Type') - @property def device_class(self) -> DeviceClass: try: @@ -221,10 +204,9 @@ def all_domains(self) -> Mapping: return result @property - def short_info(self) -> Mapping: + def short_info(self) -> Dict: keys_to_copy = ['DeviceClass', 'DeviceName', 'BuildVersion', 'ProductVersion', 'ProductType'] result = { - 'ConnectionType': self.usbmux_connection_type, 'Identifier': self.identifier, } for key in keys_to_copy: @@ -311,31 +293,19 @@ def chip_id(self) -> int: def developer_mode_status(self) -> bool: return self.get_value('com.apple.security.mac.amfi', 'DeveloperModeStatus') + def query_type(self) -> str: + return self._request('QueryType').get('Type') + def set_language(self, language: str) -> None: self.set_value(language, key='Language', domain='com.apple.international') def set_locale(self, locale: str) -> None: self.set_value(locale, key='Locale', domain='com.apple.international') - @staticmethod - def generate_host_id(hostname: str = None) -> str: - hostname = platform.node() if hostname is None else hostname - host_id = uuid.uuid3(uuid.NAMESPACE_DNS, hostname) - return str(host_id).upper() - - @reconnect_on_remote_close + @_reconnect_on_remote_close def enter_recovery(self): return self._request('EnterRecovery') - def get_system_buid(self) -> str: - result = SYSTEM_BUID - if self.medium == Medium.USBMUX: - client = usbmux.create_mux() - if isinstance(client, PlistMuxConnection): - result = client.get_buid() - client.close() - return result - def stop_session(self) -> Mapping: if self.session_id and self.service: response = self._request('StopSession', {'SessionID': self.session_id}) @@ -344,33 +314,9 @@ def stop_session(self) -> Mapping: raise CannotStopSessionError() return response - def get_itunes_pairing_record(self) -> Optional[Mapping]: - platform_type = 'linux' if not sys.platform.startswith('linux') else sys.platform - filename = LOCKDOWN_PATH[platform_type] / f'{self.identifier}.plist' - try: - with open(filename, 'rb') as f: - pair_record = plistlib.load(f) - except (PermissionError, FileNotFoundError, plistlib.InvalidFileException): - return None - return pair_record - - def get_local_pairing_record(self) -> Optional[Mapping]: - self.logger.debug('Looking for pymobiledevice3 pairing record') - - if self.pairing_records_cache_folder is None: - return None - - path = self.pairing_records_cache_folder / f'{self.identifier}.plist' - if not path.exists(): - self.logger.debug(f'No pymobiledevice3 pairing record found for device {self.identifier}') - return None - return plistlib.loads(path.read_bytes()) - def validate_pairing(self) -> bool: - try: - self._init_preferred_pair_record() - except NotPairedError: - return False + if self.pair_record is None: + self.fetch_pair_record() if self.pair_record is None: return False @@ -396,9 +342,14 @@ def validate_pairing(self) -> bool: self.service.ssl_start(f) self.paired = True + + # reload data after pairing + self.all_values = self.get_value() + self.udid = self.all_values.get('UniqueDeviceID') + return True - @reconnect_on_remote_close + @_reconnect_on_remote_close def pair(self, timeout: int = None) -> None: self.device_public_key = self.get_value('', 'DevicePublicKey') if not self.device_public_key: @@ -430,32 +381,19 @@ def pair(self, timeout: int = None) -> None: pair_record['EscrowBag'] = pair.get('EscrowBag') self.pair_record = pair_record - self._write_storage_file(f'{self.identifier}.plist', plistlib.dumps(pair_record)) - - record_data = plistlib.dumps(pair_record) - - if self.medium == Medium.USBMUX: - client = usbmux.create_mux() - if isinstance(client, PlistMuxConnection): - client.save_pair_record(self.identifier, self.service.mux_device.devid, record_data) - client.close() - + self.save_pair_record() self.paired = True - @reconnect_on_remote_close + @_reconnect_on_remote_close def unpair(self, host_id: str = None) -> None: - if host_id is not None: - pair_record = {'HostID': host_id} - self._request('Unpair', {'PairRecord': pair_record, 'ProtocolVersion': '2'}, verify_request=False) - else: - self._request('Unpair', {'PairRecord': self.pair_record, 'ProtocolVersion': '2'}, - verify_request=False) - - @reconnect_on_remote_close + pair_record = self.pair_record if host_id is None else {'HostID': host_id} + self._request('Unpair', {'PairRecord': pair_record, 'ProtocolVersion': '2'}, verify_request=False) + + @_reconnect_on_remote_close def reset_pairing(self): return self._request('ResetPairing', {'FullReset': True}) - @reconnect_on_remote_close + @_reconnect_on_remote_close def get_value(self, domain: str = None, key: str = None): options = {} @@ -471,7 +409,7 @@ def get_value(self, domain: str = None, key: str = None): return r.data return r - @reconnect_on_remote_close + @_reconnect_on_remote_close def remove_value(self, domain: str = None, key: str = None) -> Mapping: options = {} @@ -482,7 +420,7 @@ def remove_value(self, domain: str = None, key: str = None) -> Mapping: return self._request('RemoveValue', options) - @reconnect_on_remote_close + @_reconnect_on_remote_close def set_value(self, value, domain: str = None, key: str = None) -> Mapping: options = {} @@ -510,10 +448,7 @@ def get_service_connection_attributes(self, name, escrow_bag=None) -> Mapping: raise StartServiceError(response.get('Error')) return response - def _create_service_connection(self, port: int) -> ServiceConnection: - return ServiceConnection.create(self.medium, self.identifier, port, self.usbmux_connection_type) - - @reconnect_on_remote_close + @_reconnect_on_remote_close def start_service(self, name: str, escrow_bag=None) -> ServiceConnection: attr = self.get_service_connection_attributes(name, escrow_bag=escrow_bag) service_connection = self._create_service_connection(attr['Port']) @@ -561,9 +496,23 @@ def ssl_file(self) -> str: finally: os.unlink(filename) - def _write_storage_file(self, filename: Union[Path, str], data: bytes) -> None: - filepath = self.pairing_records_cache_folder / filename - filepath.write_bytes(data) + def _handle_autopair(self, autopair: bool, timeout: int) -> None: + if self.validate_pairing(): + return + + # device is not paired yet + if not autopair: + # but pairing by default was not requested + return + self.pair(timeout=timeout) + # get session_id + if not self.validate_pairing(): + raise FatalPairingError() + + @abstractmethod + def _create_service_connection(self, port: int) -> ServiceConnection: + """ Used to establish a new ServiceConnection to a given port """ + pass def _request(self, request: str, options: Mapping = None, verify_request: bool = True) -> Mapping: message = {'Label': self.label, 'Request': request} @@ -606,39 +555,115 @@ def _request_pair(self, pair_options: Mapping, timeout: int = None) -> Mapping: time.sleep(1) raise PairingDialogResponsePendingError() - def _init_preferred_pair_record(self) -> None: - """ - look for an existing pair record to connected device by following order: - - iTunes - - usbmuxd - - local storage - """ - if self.pair_record is not None: - # if already have one, use it - return + def fetch_pair_record(self) -> None: + self.pair_record = get_preferred_pair_record(self.identifier, self.pairing_records_cache_folder) - # first, look for an iTunes pair record - pair_record = self.get_itunes_pairing_record() + def save_pair_record(self) -> None: + pair_record_file = self.pairing_records_cache_folder / f'{self.identifier}.plist' + pair_record_file.write_bytes(plistlib.dumps(self.pair_record)) - if pair_record is not None: - self.logger.debug('Using iTunes pair record') - self.pair_record = pair_record - return - # second, look for usbmuxd pair record - mux = usbmux.create_mux() - if self.medium == Medium.USBMUX and isinstance(mux, PlistMuxConnection): - pair_record = mux.get_pair_record(self.identifier) - mux.close() +class UsbmuxLockdownClient(LockdownClient): + @property + def short_info(self) -> Dict: + short_info = super().short_info + short_info['ConnectionType'] = self.service.mux_device.connection_type + return short_info + + def _create_service_connection(self, port: int) -> ServiceConnection: + return ServiceConnection.create_using_usbmux(self.identifier, port, self.service.mux_device.connection_type) - if pair_record is not None: - self.logger.debug(f'Using usbmuxd pair record for identifier: {self.identifier}') - self.pair_record = pair_record - return - # lastly, look for a local pair record - pair_record = self.get_local_pairing_record() +class PlistUsbmuxLockdownClient(UsbmuxLockdownClient): + def save_pair_record(self) -> None: + super().save_pair_record() + record_data = plistlib.dumps(self.pair_record) + with usbmux.create_mux() as client: + client.save_pair_record(self.identifier, self.service.mux_device.devid, record_data) - if pair_record is not None: - self.logger.debug(f'Using local pair record: {self.identifier}.plist') - self.pair_record = pair_record + +class TcpLockdownClient(LockdownClient): + def __init__(self, service: ServiceConnection, host_id: str, hostname: 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): + """ + Create a LockdownClient instance + + :param service: lockdownd connection handler + :param host_id: Used as the host identifier for the handshake + :param hostname: The target hostname + :param identifier: Used as an identifier to look for the device pair record + :param label: lockdownd user-agent + :param system_buid: System's unique identifier + :param pair_record: Use this pair record instead of the default behavior (search in host/create our own) + :param pairing_records_cache_folder: Use the following location to search and save pair records + :param port: lockdownd service port + """ + super().__init__(service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder, + port) + self.hostname = hostname + + def _create_service_connection(self, port: int) -> ServiceConnection: + return ServiceConnection.create_using_tcp(self.hostname, port) + + +def create_using_usbmux(serial: str = None, identifier: str = None, label: str = DEFAULT_LABEL, autopair: bool = True, + connection_type: str = None, pair_timeout: int = None, local_hostname: str = None, + pair_record: Mapping = None, pairing_records_cache_folder: Path = None, + port: int = SERVICE_PORT) -> UsbmuxLockdownClient: + """ + Create a UsbmuxLockdownClient instance + + :param serial: Usbmux serial identifier + :param identifier: Used as an identifier to look for the device pair record + :param label: lockdownd user-agent + :param autopair: Attempt to pair with device (blocking) if not already paired + :param connection_type: Force a specific type of usbmux connection (USB/Network) + :param pair_timeout: Timeout for autopair + :param local_hostname: Used as a seed to generate the HostID + :param pair_record: Use this pair record instead of the default behavior (search in host/create our own) + :param pairing_records_cache_folder: Use the following location to search and save pair records + :param port: lockdownd service port + :return: UsbmuxLockdownClient instance + """ + service = ServiceConnection.create_using_usbmux(serial, port, connection_type=connection_type) + cls = UsbmuxLockdownClient + with usbmux.create_mux() as client: + if isinstance(client, PlistMuxConnection): + # Only the Plist version of usbmuxd supports this message type + system_buid = client.get_buid() + cls = PlistUsbmuxLockdownClient + + if identifier is None: + # attempt get identifier from mux device serial + identifier = service.mux_device.serial + + return cls.create( + service, identifier=identifier, label=label, system_buid=system_buid, local_hostname=local_hostname, + pair_record=pair_record, pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, + autopair=autopair) + + +def create_using_tcp(hostname: str, identifier: str = None, label: str = DEFAULT_LABEL, autopair: bool = True, + pair_timeout: int = None, local_hostname: str = None, pair_record: Mapping = None, + pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT) -> TcpLockdownClient: + """ + Create a TcpLockdownClient instance + + :param hostname: The target device hostname + :param identifier: Used as an identifier to look for the device pair record + :param label: lockdownd user-agent + :param autopair: Attempt to pair with device (blocking) if not already paired + :param pair_timeout: Timeout for autopair + :param local_hostname: Used as a seed to generate the HostID + :param pair_record: Use this pair record instead of the default behavior (search in host/create our own) + :param pairing_records_cache_folder: Use the following location to search and save pair records + :param port: lockdownd service port + :return: TcpLockdownClient instance + """ + service = ServiceConnection.create_using_tcp(hostname, port) + client = TcpLockdownClient.create( + service, identifier=identifier, label=label, local_hostname=local_hostname, pair_record=pair_record, + pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, autopair=autopair, + port=port, hostname=hostname) + return client diff --git a/pymobiledevice3/pair_records.py b/pymobiledevice3/pair_records.py new file mode 100644 index 000000000..e4260b9b2 --- /dev/null +++ b/pymobiledevice3/pair_records.py @@ -0,0 +1,81 @@ +import logging +import os +import platform +import plistlib +import sys +import uuid +from contextlib import suppress +from pathlib import Path +from typing import Mapping, Optional + +from pymobiledevice3 import usbmux +from pymobiledevice3.common import get_home_folder +from pymobiledevice3.exceptions import MuxException, NotPairedError +from pymobiledevice3.usbmux import PlistMuxConnection + +PAIR_RECORDS_PATH = { + 'win32': Path(os.environ.get('ALLUSERSPROFILE', ''), 'Apple', 'Lockdown'), + 'darwin': Path('/var/db/lockdown/'), + 'linux': Path('/var/lib/lockdown/'), +} + +logger = logging.getLogger(__name__) + + +def generate_host_id(hostname: str = None) -> str: + hostname = platform.node() if hostname is None else hostname + host_id = uuid.uuid3(uuid.NAMESPACE_DNS, hostname) + return str(host_id).upper() + + +def get_itunes_pairing_record(identifier: str) -> Optional[Mapping]: + platform_type = 'linux' if not sys.platform.startswith('linux') else sys.platform + filename = PAIR_RECORDS_PATH[platform_type] / f'{identifier}.plist' + try: + with open(filename, 'rb') as f: + pair_record = plistlib.load(f) + except (PermissionError, FileNotFoundError, plistlib.InvalidFileException): + return None + return pair_record + + +def get_local_pairing_record(identifier: str, pairing_records_cache_folder: Path) -> Optional[Mapping]: + logger.debug('Looking for pymobiledevice3 pairing record') + path = pairing_records_cache_folder / f'{identifier}.plist' + if not path.exists(): + logger.debug(f'No pymobiledevice3 pairing record found for device {identifier}') + return None + return plistlib.loads(path.read_bytes()) + + +def get_preferred_pair_record(identifier: str, pairing_records_cache_folder: Path) -> Mapping: + """ + look for an existing pair record to connected device by following order: + - usbmuxd + - iTunes + - local storage + """ + + # usbmuxd + with suppress(NotPairedError, MuxException): + with usbmux.create_mux() as mux: + if isinstance(mux, PlistMuxConnection): + pair_record = mux.get_pair_record(identifier) + if pair_record is not None: + return pair_record + + # iTunes + pair_record = get_itunes_pairing_record(identifier) + if pair_record is not None: + return pair_record + + # local storage + return get_local_pairing_record(identifier, pairing_records_cache_folder) + + +def create_pairing_records_cache_folder(pairing_records_cache_folder: Path = None) -> Path: + if pairing_records_cache_folder is None: + pairing_records_cache_folder = get_home_folder() + else: + pairing_records_cache_folder.mkdir(parents=True, exist_ok=True) + return pairing_records_cache_folder diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 967a82ad9..e95c91945 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -1,6 +1,6 @@ import pytest -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux @pytest.fixture(scope='function') @@ -8,5 +8,5 @@ def lockdown(): """ Creates a new lockdown client for each test. """ - with LockdownClient() as client: + with create_using_usbmux() as client: yield client diff --git a/tests/services/test_crash_reports.py b/tests/services/test_crash_reports.py index 052d027db..0e6118a06 100644 --- a/tests/services/test_crash_reports.py +++ b/tests/services/test_crash_reports.py @@ -3,7 +3,7 @@ import pytest -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.services.crash_reports import CrashReportsManager BASENAME = '__pymobiledevice3_tests' @@ -19,7 +19,7 @@ def crash_manager(lockdown): @pytest.fixture(scope='module', autouse=True) def delete_test_dir(): yield - with LockdownClient() as lockdown_client: + with create_using_usbmux() as lockdown_client: with CrashReportsManager(lockdown_client) as crash_manager: crash_manager.afc.rm(BASENAME) diff --git a/tests/services/test_dvt_secure_socket_proxy.py b/tests/services/test_dvt_secure_socket_proxy.py index 21814c2ab..6cdd476b4 100644 --- a/tests/services/test_dvt_secure_socket_proxy.py +++ b/tests/services/test_dvt_secure_socket_proxy.py @@ -3,7 +3,7 @@ import pytest from pymobiledevice3.exceptions import AlreadyMountedError, DvtDirListError, UnrecognizedSelectorError -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService from pymobiledevice3.services.dvt.instruments.application_listing import ApplicationListing from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo @@ -16,7 +16,7 @@ @pytest.fixture(scope='module', autouse=True) def mount_developer_disk_image(): - with LockdownClient() as lockdown: + with create_using_usbmux() as lockdown: with DeveloperDiskImageMounter(lockdown=lockdown) as mounter: if mounter.is_image_mounted('Developer'): yield diff --git a/tests/services/test_tcp_forwarder.py b/tests/services/test_tcp_forwarder.py index 637398109..6afcef6e8 100644 --- a/tests/services/test_tcp_forwarder.py +++ b/tests/services/test_tcp_forwarder.py @@ -3,7 +3,7 @@ import pytest -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import SERVICE_PORT, LockdownClient from pymobiledevice3.tcp_forwarder import TcpForwarder FREE_PORT = 3582 @@ -15,7 +15,7 @@ def attempt_local_connection(port: int): client.close() -@pytest.mark.parametrize('dst_port', [FREE_PORT, LockdownClient.SERVICE_PORT]) +@pytest.mark.parametrize('dst_port', [FREE_PORT, SERVICE_PORT]) def test_tcp_forwarder_bad_port(lockdown: LockdownClient, dst_port: int): # start forwarder listening_event = threading.Event() From 1e66df71366b65c30a66e05e90143695dca97617 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 18 Jun 2023 09:35:12 +0300 Subject: [PATCH 085/234] remove python3.7 support --- .github/workflows/python-app.yml | 2 +- pymobiledevice3/restore/device.py | 3 +-- pymobiledevice3/restore/restored_client.py | 3 +-- pyproject.toml | 1 - requirements.txt | 1 - 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c2b787479..e7c3958f4 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] + python-version: [ 3.8, 3.9, "3.10", "3.11" ] os: [ ubuntu-latest, macos-latest, windows-latest ] steps: diff --git a/pymobiledevice3/restore/device.py b/pymobiledevice3/restore/device.py index 9de3214a6..01920075b 100644 --- a/pymobiledevice3/restore/device.py +++ b/pymobiledevice3/restore/device.py @@ -1,6 +1,5 @@ from contextlib import suppress - -from cached_property import cached_property +from functools import cached_property from pymobiledevice3.exceptions import MissingValueError from pymobiledevice3.irecv import IRecv diff --git a/pymobiledevice3/restore/restored_client.py b/pymobiledevice3/restore/restored_client.py index 279ab5937..5bfca990c 100644 --- a/pymobiledevice3/restore/restored_client.py +++ b/pymobiledevice3/restore/restored_client.py @@ -1,6 +1,5 @@ import logging - -from cached_property import cached_property +from functools import cached_property from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError, NoDeviceConnectedError diff --git a/pyproject.toml b/pyproject.toml index 8cc35af66..3c26919e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/requirements.txt b/requirements.txt index 052476e2a..9f6c5b207 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,6 @@ gpxpy pykdebugparser>=1.2.4 pyusb>=1.2.1 tqdm -cached-property requests cmd2 packaging From a6bd87c2fee1bbd81628f28f843bce2060641df9 Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 18 Jun 2023 10:07:36 +0300 Subject: [PATCH 086/234] exceptions: add `InvalidConnectionError` --- pymobiledevice3/exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index ab227b7eb..b394a0f50 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -219,6 +219,10 @@ class MissingValueError(LockdownError): pass +class InvalidConnectionError(LockdownError): + pass + + class PasscodeRequiredError(LockdownError): """ passcode must be present for this action """ pass From 211ca7f9a7eea77ff6eee17bde6105126aae430e Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Sun, 18 Jun 2023 10:12:47 +0300 Subject: [PATCH 087/234] TcpLockdownClient: handle no identifier errors --- pymobiledevice3/lockdown.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index a4b27f684..d27248ff3 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -17,9 +17,9 @@ from pymobiledevice3 import usbmux from pymobiledevice3.ca import ca_do_everything from pymobiledevice3.exceptions import CannotStopSessionError, ConnectionTerminatedError, FatalPairingError, \ - IncorrectModeError, InvalidHostIDError, InvalidServiceError, LockdownError, MissingValueError, NotPairedError, \ - PairingDialogResponsePendingError, PairingError, PasswordRequiredError, SetProhibitedError, StartServiceError, \ - UserDeniedPairingError + IncorrectModeError, InvalidConnectionError, InvalidHostIDError, InvalidServiceError, LockdownError, \ + MissingValueError, NotPairedError, PairingDialogResponsePendingError, PairingError, PasswordRequiredError, \ + SetProhibitedError, StartServiceError, UserDeniedPairingError from pymobiledevice3.irecv_devices import IRECV_DEVICES from pymobiledevice3.pair_records import create_pairing_records_cache_folder, generate_host_id, \ get_preferred_pair_record @@ -160,10 +160,7 @@ def create(cls, service: ServiceConnection, identifier: str = None, system_buid: :return: LockdownClient subclass """ host_id = generate_host_id(local_hostname) - pairing_records_cache_folder = create_pairing_records_cache_folder(pairing_records_cache_folder) - if pair_record is None: - pair_record = get_preferred_pair_record(identifier, pairing_records_cache_folder) lockdown_client = cls( service, host_id=host_id, identifier=identifier, label=label, system_buid=system_buid, @@ -332,7 +329,7 @@ def validate_pairing(self) -> bool: try: start_session = self._request('StartSession', {'HostID': self.host_id, 'SystemBUID': self.system_buid}) - except InvalidHostIDError: + except (InvalidHostIDError, InvalidConnectionError): # no host id means there is no such pairing record return False @@ -531,7 +528,8 @@ def _request(self, request: str, options: Mapping = None, verify_request: bool = 'InvalidHostID': InvalidHostIDError, 'SetProhibited': SetProhibitedError, 'MissingValue': MissingValueError, - 'InvalidService': InvalidServiceError, } + 'InvalidService': InvalidServiceError, + 'InvalidConnection': InvalidConnectionError, } raise exception_errors.get(error, LockdownError)(error) # iOS < 5: 'Error' is not present, so we need to check the 'Result' instead @@ -556,7 +554,8 @@ def _request_pair(self, pair_options: Mapping, timeout: int = None) -> Mapping: raise PairingDialogResponsePendingError() def fetch_pair_record(self) -> None: - self.pair_record = get_preferred_pair_record(self.identifier, self.pairing_records_cache_folder) + if self.identifier is not None: + self.pair_record = get_preferred_pair_record(self.identifier, self.pairing_records_cache_folder) def save_pair_record(self) -> None: pair_record_file = self.pairing_records_cache_folder / f'{self.identifier}.plist' From 2ea6f90e1c6aa21c9e81e2613fd85061cf3953d0 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 18 Jun 2023 06:31:00 -0700 Subject: [PATCH 088/234] README: update `LockdownClient` creation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be9bd604d..371d90ad3 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,10 @@ Commands: Or import the modules and use the API yourself: ```python -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.services.syslog import SyslogService -lockdown = LockdownClient() +lockdown = create_using_usbmux() for line in SyslogService(lockdown=lockdown).watch(): # just print all syslog lines as is print(line) From fb6a27bc8f8be44b9d8c31e2373794f1e7e6cc16 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 18 Jun 2023 16:37:26 +0300 Subject: [PATCH 089/234] pyproject: bump version to 2.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c26919e9..1e1462a62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "1.42.4" +version = "2.0.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.7" From 84c4437fa7600cd3ca3e9c0f96b526ba683d9bed Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 18 Jun 2023 17:37:00 +0300 Subject: [PATCH 090/234] pair_records: fix `get_preferred_pair_record()` on non-plist usbmuxd --- pymobiledevice3/pair_records.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/pair_records.py b/pymobiledevice3/pair_records.py index e4260b9b2..73397e0f1 100644 --- a/pymobiledevice3/pair_records.py +++ b/pymobiledevice3/pair_records.py @@ -61,8 +61,8 @@ def get_preferred_pair_record(identifier: str, pairing_records_cache_folder: Pat with usbmux.create_mux() as mux: if isinstance(mux, PlistMuxConnection): pair_record = mux.get_pair_record(identifier) - if pair_record is not None: - return pair_record + if pair_record is not None: + return pair_record # iTunes pair_record = get_itunes_pairing_record(identifier) From 0436298409e646ae153fe92131a3c79910a8a469 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 18 Jun 2023 17:45:25 +0300 Subject: [PATCH 091/234] pyproject: bump version to 2.0.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e1462a62..0c0af8ce7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.0.0" +version = "2.0.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.7" From ea1dc5572dc5d65dfedeb22313e39445c5ca9faf Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 19 Jun 2023 12:58:43 +0300 Subject: [PATCH 092/234] pyproject: update `requires-python` key to `>=3.8` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0c0af8ce7..3bdd96a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "pymobiledevice3" version = "2.0.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = { file = "LICENSE" } keywords = ["ios", "protocol", "lockdownd", "instruments", "automation", "cli", "afc"] authors = [ From 583975b3a647df2b9f6879a18c8bfe691d7be2d6 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 19 Jun 2023 12:59:21 +0300 Subject: [PATCH 093/234] python-app: remove python3.7 specific test --- .github/workflows/python-app.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index e7c3958f4..ac66d3a1c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -37,7 +37,6 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -U . - - if: (!((matrix.os == 'windows-latest') && (matrix.python-version == '3.7'))) - name: Test show usage + - name: Test show usage run: | python -m pymobiledevice3 From a11c8a4d60f4a2db3c1e5581d38a562e38569bb5 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 19 Jun 2023 13:00:04 +0300 Subject: [PATCH 094/234] fix old references to lockdown creation using `LockdownClient()` --- pymobiledevice3/cli/restore.py | 4 ++-- pymobiledevice3/cli/webinspector.py | 4 ++-- pymobiledevice3/restore/recovery.py | 4 ++-- pymobiledevice3/services/amfi.py | 4 ++-- pymobiledevice3/services/mobile_activation.py | 6 +++--- pymobiledevice3/tcp_forwarder.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pymobiledevice3/cli/restore.py b/pymobiledevice3/cli/restore.py index f1375a1da..6d962ffdd 100644 --- a/pymobiledevice3/cli/restore.py +++ b/pymobiledevice3/cli/restore.py @@ -13,7 +13,7 @@ from pymobiledevice3.cli.cli_common import print_json, set_verbosity from pymobiledevice3.exceptions import IncorrectModeError from pymobiledevice3.irecv import IRecv -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux from pymobiledevice3.restore.device import Device from pymobiledevice3.restore.recovery import Behavior, Recovery from pymobiledevice3.restore.restore import Restore @@ -46,7 +46,7 @@ def device(ctx, param, value): logger.debug('searching among connected devices via lockdownd') for device in usbmux.list_devices(): try: - lockdown = LockdownClient(serial=device.serial) + lockdown = create_using_usbmux(serial=device.serial) except IncorrectModeError: continue if (ecid is None) or (lockdown.ecid == value): diff --git a/pymobiledevice3/cli/webinspector.py b/pymobiledevice3/cli/webinspector.py index c23f41680..717c7fe10 100644 --- a/pymobiledevice3/cli/webinspector.py +++ b/pymobiledevice3/cli/webinspector.py @@ -23,7 +23,7 @@ from pymobiledevice3.common import get_home_folder from pymobiledevice3.exceptions import InspectorEvaluateError, LaunchingApplicationError, \ RemoteAutomationNotEnabledError, WebInspectorNotEnabledError, WirError -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux from pymobiledevice3.services.web_protocol.cdp_server import app from pymobiledevice3.services.web_protocol.driver import By, Cookie, WebDriver from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession @@ -204,7 +204,7 @@ def js_shell(lockdown: LockdownClient, timeout, automation, url): def create_app(): - inspector = WebinspectorService(lockdown=LockdownClient(udid)) + inspector = WebinspectorService(lockdown=create_using_usbmux(udid)) app.state.inspector = inspector return app diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index 62e6444fc..5d2dadf1f 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -8,7 +8,7 @@ from pymobiledevice3.exceptions import PyMobileDevice3Exception from pymobiledevice3.irecv import IRecv, Mode -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.restore.base_restore import BaseRestore, Behavior from pymobiledevice3.restore.consts import lpol_file from pymobiledevice3.restore.device import Device @@ -449,7 +449,7 @@ def boot_ramdisk(self): self.logger.info('going into Recovery') # in case lockdown has disconnected while waiting for a ticket - self.device.lockdown = LockdownClient(serial=self.device.lockdown.udid) + self.device.lockdown = create_using_usbmux(serial=self.device.lockdown.udid) self.device.lockdown.enter_recovery() self.device.lockdown = None diff --git a/pymobiledevice3/services/amfi.py b/pymobiledevice3/services/amfi.py index d53c3f50e..2a6ca70f5 100644 --- a/pymobiledevice3/services/amfi.py +++ b/pymobiledevice3/services/amfi.py @@ -5,7 +5,7 @@ from pymobiledevice3.exceptions import AmfiError, BadDevError, ConnectionFailedError, DeveloperModeError, \ DeviceHasPasscodeSetError, NoDeviceConnectedError, PyMobileDevice3Exception -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux from pymobiledevice3.services.heartbeat import HeartbeatService @@ -51,7 +51,7 @@ def enable_developer_mode(self, enable_post_restart=True): while True: try: - self._lockdown = LockdownClient(self._lockdown.udid) + self._lockdown = create_using_usbmux(self._lockdown.udid) break except (NoDeviceConnectedError, ConnectionFailedError, BadDevError, construct.core.StreamError): pass diff --git a/pymobiledevice3/services/mobile_activation.py b/pymobiledevice3/services/mobile_activation.py index 83a31e64e..07bd7f960 100755 --- a/pymobiledevice3/services/mobile_activation.py +++ b/pymobiledevice3/services/mobile_activation.py @@ -5,7 +5,7 @@ import requests -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux ACTIVATION_USER_AGENT_IOS = 'iOS Device Activator (MobileActivation-20 built on Jan 15 2012 at 19:07:28)' ACTIVATION_DEFAULT_URL = 'https://albert.apple.com/deviceservices/deviceActivation' @@ -72,14 +72,14 @@ def activate_with_session(self, activation_record, headers): } if headers: data['ActivationResponseHeaders'] = dict(headers) - with closing(LockdownClient(self.lockdown.udid).start_service(self.SERVICE_NAME)) as service: + with closing(create_using_usbmux(self.lockdown.udid).start_service(self.SERVICE_NAME)) as service: return service.send_recv_plist(data) def send_command(self, command, value=''): data = {'Command': command} if value: data['Value'] = value - with closing(LockdownClient(self.lockdown.udid).start_service(self.SERVICE_NAME)) as service: + with closing(create_using_usbmux(self.lockdown.udid).start_service(self.SERVICE_NAME)) as service: return service.send_recv_plist(data) def post(self, url, data, headers=None): diff --git a/pymobiledevice3/tcp_forwarder.py b/pymobiledevice3/tcp_forwarder.py index 7b5860f43..f164ff108 100644 --- a/pymobiledevice3/tcp_forwarder.py +++ b/pymobiledevice3/tcp_forwarder.py @@ -5,7 +5,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.service_connection import ServiceConnection @@ -119,7 +119,7 @@ def _handle_server_connection(self): try: if self.enable_ssl: # use the lockdown pairing record - lockdown = LockdownClient(self.serial, usbmux_connection_type=self.usbmux_connection_type) + lockdown = create_using_usbmux(self.serial, connection_type=self.usbmux_connection_type) service_connection = ServiceConnection.create_using_usbmux( self.serial, self.dst_port, connection_type=self.usbmux_connection_type) From 31704b710d9b4863ee6c2d75f13521fe58a9aac0 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 19 Jun 2023 13:21:44 +0300 Subject: [PATCH 095/234] pyproject: bump version to 2.0.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3bdd96a16..9d437a51f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.0.1" +version = "2.0.2" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From bd10b655c55b771f9e28c9b30afb120b7ae481b8 Mon Sep 17 00:00:00 2001 From: Lorenz Sieben Date: Fri, 16 Jun 2023 17:43:40 +0200 Subject: [PATCH 096/234] Add supervised configuration profile installs --- pymobiledevice3/cli/profile.py | 15 +++++++++++++++ pymobiledevice3/services/mobile_config.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/pymobiledevice3/cli/profile.py b/pymobiledevice3/cli/profile.py index 42883763c..9320c6ed8 100644 --- a/pymobiledevice3/cli/profile.py +++ b/pymobiledevice3/cli/profile.py @@ -37,6 +37,21 @@ def profile_install(lockdown: LockdownClient, profiles): service.install_profile(profile.read()) +@profile_group.command('install-silent', cls=Command) +@click.option('--keystore', type=click.File('rb'), required=True, + help="A PKCS#12 keystore containing the certificate and private key which can supervise the device.") +@click.option('--keystore-password', prompt=True, required=True, hide_input=True, + help="The password for the PKCS#12 keystore.") +@click.argument('profiles', nargs=-1, type=click.File('rb')) +def profile_install_silent(lockdown: LockdownClient, profiles, keystore, keystore_password): + """ install given profiles without user interaction (requires the device to be supervised) """ + service = MobileConfigService(lockdown=lockdown) + for profile in profiles: + logger.info(f'installing {profile.name}') + service.install_profile_silent( + profile.read(), keystore.read(), keystore_password) + + @profile_group.command('cloud-configuration', cls=Command) @click.option('--color/--no-color', default=True) def profile_cloud_configuration(lockdown: LockdownClient, color): diff --git a/pymobiledevice3/services/mobile_config.py b/pymobiledevice3/services/mobile_config.py index e06ee0ade..19a3d16e8 100755 --- a/pymobiledevice3/services/mobile_config.py +++ b/pymobiledevice3/services/mobile_config.py @@ -5,6 +5,10 @@ from pymobiledevice3.exceptions import ProfileError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.base_service import BaseService +from cryptography.hazmat.primitives.serialization.pkcs12 import load_pkcs12 +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.serialization.pkcs7 import PKCS7SignatureBuilder +from cryptography.hazmat.primitives import hashes class Purpose(Enum): @@ -23,6 +27,18 @@ def hello(self) -> None: def flush(self) -> None: self._send_recv({'RequestType': 'Flush'}) + def escalate(self, pkcs12: bytes, password: str) -> None: + decrypted_p12 = load_pkcs12(pkcs12, password.encode('utf-8')) + + escalate_response = self._send_recv({ + 'RequestType': 'Escalate', + 'SupervisorCertificate': decrypted_p12.cert.certificate.public_bytes(Encoding.DER) + }) + signed_challenge = PKCS7SignatureBuilder().set_data(escalate_response['Challenge']).add_signer( + decrypted_p12.cert.certificate, decrypted_p12.key, hashes.SHA256()).sign(Encoding.DER, []) + self._send_recv({'RequestType': 'EscalateResponse', 'SignedRequest': signed_challenge}) + self._send_recv({'RequestType': 'ProceedWithKeybagMigration'}) + def get_stored_profile(self, purpose: Purpose = Purpose.PostSetupInstallation) -> Mapping: return self._send_recv({'RequestType': 'GetStoredProfile', 'Purpose': purpose.value}) @@ -54,6 +70,10 @@ def get_profile_list(self) -> Mapping: def install_profile(self, payload: bytes) -> None: self._send_recv({'RequestType': 'InstallProfile', 'Payload': payload}) + def install_profile_silent(self, profile: bytes, pkcs12: bytes, password: str) -> None: + self.escalate(pkcs12, password) + self._send_recv({'RequestType': 'InstallProfileSilent', 'Payload': profile}) + def remove_profile(self, identifier: str) -> None: profiles = self.get_profile_list() if not profiles: From 0515798316bd4837fc04feb9a951a6de911122f4 Mon Sep 17 00:00:00 2001 From: Matan Perelman Date: Wed, 5 Jul 2023 10:34:53 +0300 Subject: [PATCH 097/234] workflow: Run CI for pull requests to master --- .github/workflows/python-app.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ac66d3a1c..71876364f 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -6,6 +6,9 @@ name: Python application on: push: branches: [ '**' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] jobs: build: From 19c3301544da7968e2508db81a411e7ea977bb21 Mon Sep 17 00:00:00 2001 From: Matan Perelman Date: Wed, 5 Jul 2023 10:37:01 +0300 Subject: [PATCH 098/234] mobile_config: Reorder imports --- pymobiledevice3/services/mobile_config.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/services/mobile_config.py b/pymobiledevice3/services/mobile_config.py index 19a3d16e8..a2829f6e6 100755 --- a/pymobiledevice3/services/mobile_config.py +++ b/pymobiledevice3/services/mobile_config.py @@ -2,13 +2,14 @@ from enum import Enum from typing import Mapping +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.serialization.pkcs7 import PKCS7SignatureBuilder +from cryptography.hazmat.primitives.serialization.pkcs12 import load_pkcs12 + from pymobiledevice3.exceptions import ProfileError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.base_service import BaseService -from cryptography.hazmat.primitives.serialization.pkcs12 import load_pkcs12 -from cryptography.hazmat.primitives.serialization import Encoding -from cryptography.hazmat.primitives.serialization.pkcs7 import PKCS7SignatureBuilder -from cryptography.hazmat.primitives import hashes class Purpose(Enum): From dc7783c2a390a36be1fe2e215f7cd433c6aa1662 Mon Sep 17 00:00:00 2001 From: Guy Salton Date: Sun, 9 Jul 2023 17:58:56 +0300 Subject: [PATCH 099/234] cli: use inquirer3 package instead of inquirer --- pymobiledevice3/cli/cli_common.py | 8 ++++---- pymobiledevice3/cli/webinspector.py | 8 ++++---- requirements.txt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index 3e4179c51..ff5c47910 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -7,8 +7,8 @@ import click import coloredlogs import hexdump -import inquirer -from inquirer.themes import GreenPassion +import inquirer3 +from inquirer3.themes import GreenPassion from pygments import formatters, highlight, lexers from pymobiledevice3.exceptions import NoDeviceSelectedError @@ -98,9 +98,9 @@ def udid(ctx, param, value): device_info = DeviceInfo(lockdown_client) devices_options.append(device_info) - device_question = [inquirer.List('device', message='choose device', choices=devices_options, carousel=True)] + device_question = [inquirer3.List('device', message='choose device', choices=devices_options, carousel=True)] try: - result = inquirer.prompt(device_question, theme=GreenPassion(), raise_keyboard_interrupt=True) + result = inquirer3.prompt(device_question, theme=GreenPassion(), raise_keyboard_interrupt=True) return result['device'].lockdown_client except KeyboardInterrupt as e: raise NoDeviceSelectedError from e diff --git a/pymobiledevice3/cli/webinspector.py b/pymobiledevice3/cli/webinspector.py index 717c7fe10..32334d81f 100644 --- a/pymobiledevice3/cli/webinspector.py +++ b/pymobiledevice3/cli/webinspector.py @@ -6,10 +6,10 @@ from typing import Optional, Type import click -import inquirer +import inquirer3 import IPython import uvicorn -from inquirer.themes import GreenPassion +from inquirer3.themes import GreenPassion from prompt_toolkit import HTML, PromptSession from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.history import FileHistory @@ -336,8 +336,8 @@ def query_page(inspector: WebinspectorService) -> Optional[Page]: logger.error('Unable to find available pages (try to unlock device)') return - page_query = [inquirer.List('page', message='choose page', choices=available_pages, carousel=True)] - page = inquirer.prompt(page_query, theme=GreenPassion(), raise_keyboard_interrupt=True)['page'] + page_query = [inquirer3.List('page', message='choose page', choices=available_pages, carousel=True)] + page = inquirer3.prompt(page_query, theme=GreenPassion(), raise_keyboard_interrupt=True)['page'] return page diff --git a/requirements.txt b/requirements.txt index 9f6c5b207..7ffca5cc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ starlette wsproto nest_asyncio>=1.5.5 Pillow -inquirer +inquirer3>=0.1.0 pyimg4>=0.7 ipsw_parser>=1.1.2 remotezip From 9d4577a65159ce5e14ef8bb041d8a4eaa23b87fd Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 9 Jul 2023 13:56:39 +0300 Subject: [PATCH 100/234] cli_common: add `prompt_device_list()` --- pymobiledevice3/cli/cli_common.py | 35 +++++++++---------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index ff5c47910..7418d1870 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -3,6 +3,7 @@ import logging import os import uuid +from typing import List, Optional import click import coloredlogs @@ -55,18 +56,13 @@ def wait_return(): UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID' -class DeviceInfo: - def __init__(self, lockdown_client: LockdownClient): - self.lockdown_client = lockdown_client - self.product_version = self.lockdown_client.product_version - self.serial = self.lockdown_client.identifier - self.display_name = self.lockdown_client.display_name - - def __str__(self): - if self.display_name is None: - return f'Unknown device, ios version: {self.product_version}, serial: {self.serial}' - else: - return f'{self.display_name}, ios version: {self.product_version}, serial: {self.serial}' +def prompt_device_list(device_list: List): + device_question = [inquirer3.List('device', message='choose device', choices=device_list, carousel=True)] + try: + result = inquirer3.prompt(device_question, theme=GreenPassion(), raise_keyboard_interrupt=True) + return result['device'] + except KeyboardInterrupt: + raise NoDeviceSelectedError() class Command(click.Command): @@ -80,7 +76,7 @@ def __init__(self, *args, **kwargs): ] @staticmethod - def udid(ctx, param, value): + def udid(ctx, param: str, value: str) -> Optional[LockdownClient]: if '_PYMOBILEDEVICE3_COMPLETE' in os.environ: # prevent lockdown connection establishment when in autocomplete mode return @@ -92,18 +88,7 @@ def udid(ctx, param, value): if len(devices) <= 1: return create_using_usbmux() - devices_options = [] - for device in devices: - lockdown_client = create_using_usbmux(serial=device.serial) - device_info = DeviceInfo(lockdown_client) - devices_options.append(device_info) - - device_question = [inquirer3.List('device', message='choose device', choices=devices_options, carousel=True)] - try: - result = inquirer3.prompt(device_question, theme=GreenPassion(), raise_keyboard_interrupt=True) - return result['device'].lockdown_client - except KeyboardInterrupt as e: - raise NoDeviceSelectedError from e + return prompt_device_list([create_using_usbmux(serial=device.serial) for device in devices]) class CommandWithoutAutopair(Command): From 004f3f98c43784d8684a7149e74e791d636d4772 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 6 Jul 2023 17:16:05 +0300 Subject: [PATCH 101/234] cli: add `lockdown unpair [hostid]` argument --- pymobiledevice3/cli/lockdown.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/cli/lockdown.py b/pymobiledevice3/cli/lockdown.py index 2810ee1b4..398e9dea2 100644 --- a/pymobiledevice3/cli/lockdown.py +++ b/pymobiledevice3/cli/lockdown.py @@ -72,9 +72,10 @@ def lockdown_remove(lockdown: LockdownClient, domain, key, color): @lockdown_group.command('unpair', cls=CommandWithoutAutopair) -def lockdown_unpair(lockdown: LockdownClient): +@click.argument('host_id', required=False) +def lockdown_unpair(lockdown: LockdownClient, host_id: str = None): """ unpair from connected device """ - lockdown.unpair() + lockdown.unpair(host_id=host_id) @lockdown_group.command('pair', cls=CommandWithoutAutopair) From 803fa81b906b5d17e62f2f150d0edcd9e1ed7034 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 9 Jul 2023 14:05:55 +0300 Subject: [PATCH 102/234] add `remoted` discover using bonjour --- pymobiledevice3/remote/__init__.py | 0 pymobiledevice3/remote/bonjour.py | 39 ++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 40 insertions(+) create mode 100644 pymobiledevice3/remote/__init__.py create mode 100644 pymobiledevice3/remote/bonjour.py diff --git a/pymobiledevice3/remote/__init__.py b/pymobiledevice3/remote/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pymobiledevice3/remote/bonjour.py b/pymobiledevice3/remote/bonjour.py new file mode 100644 index 000000000..75ec66ea4 --- /dev/null +++ b/pymobiledevice3/remote/bonjour.py @@ -0,0 +1,39 @@ +import itertools +import time +from socket import AF_INET6, inet_ntop +from typing import List + +from ifaddr import Adapter, get_adapters +from zeroconf import ServiceBrowser, ServiceListener, Zeroconf +from zeroconf.const import _TYPE_AAAA + +DEFAULT_BONJOUR_TIMEOUT = 1 + + +class RemotedListener(ServiceListener): + def __init__(self, adapter: Adapter): + super().__init__() + self.adapter = adapter + self.addresses: List[str] = [] + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + if name == 'ncm._remoted._tcp.local.': + service_info = zc.get_service_info(type_, name) + entries_with_name = zc.cache.async_entries_with_name(service_info.server) + for entry in entries_with_name: + if entry.type == _TYPE_AAAA: + self.addresses.append(inet_ntop(AF_INET6, entry.address) + '%' + self.adapter.nice_name) + + +def query_bonjour(adapter: Adapter) -> RemotedListener: + zeroconf = Zeroconf(interfaces=[adapter.ips[0].ip[0]]) + listener = RemotedListener(adapter) + ServiceBrowser(zeroconf, '_remoted._tcp.local.', listener) + return listener + + +def get_remoted_addresses(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[str]: + adapters = [adapter for adapter in get_adapters() if adapter.ips[0].is_IPv6] + listeners = [query_bonjour(adapter) for adapter in adapters] + time.sleep(timeout) + return list(itertools.chain.from_iterable([listener.addresses for listener in listeners])) diff --git a/requirements.txt b/requirements.txt index 7ffca5cc4..556c17d12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ pyimg4>=0.7 ipsw_parser>=1.1.2 remotezip zeroconf +ifaddr From c20cb16e4db14042d860e5f1ac4187bef39896f8 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 9 Jul 2023 14:08:22 +0300 Subject: [PATCH 103/234] remote: add RSD implementation --- .../remote/remote_service_discovery.py | 59 ++++ pymobiledevice3/remote/remotexpc.py | 101 +++++++ pymobiledevice3/remote/xpc_message.py | 267 ++++++++++++++++++ requirements.txt | 1 + 4 files changed, 428 insertions(+) create mode 100644 pymobiledevice3/remote/remote_service_discovery.py create mode 100644 pymobiledevice3/remote/remotexpc.py create mode 100644 pymobiledevice3/remote/xpc_message.py diff --git a/pymobiledevice3/remote/remote_service_discovery.py b/pymobiledevice3/remote/remote_service_discovery.py new file mode 100644 index 000000000..5c3342c14 --- /dev/null +++ b/pymobiledevice3/remote/remote_service_discovery.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from typing import List, Tuple + +from pymobiledevice3.exceptions import NoDeviceConnectedError +from pymobiledevice3.remote.bonjour import DEFAULT_BONJOUR_TIMEOUT, get_remoted_addresses +from pymobiledevice3.remote.remotexpc import RemoteXPCConnection + + +@dataclass +class RSDDevice: + hostname: str + udid: str + product_type: str + os_version: str + + +# from remoted ([RSDRemoteNCMDeviceDevice createPortListener]) +RSD_PORT = 58783 + + +class RemoteServiceDiscoveryService: + def __init__(self, address: Tuple[str, int]): + self.service = RemoteXPCConnection(address) + self.peer_info = None + + 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)) + service.connect() + return service + + def __enter__(self) -> 'RemoteServiceDiscoveryService': + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.service.close() + + +def get_remoted_devices(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[RSDDevice]: + result = [] + for hostname in get_remoted_addresses(timeout): + with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd: + properties = rsd.peer_info['Properties'] + result.append(RSDDevice(hostname=hostname, udid=properties['UniqueDeviceID'], + product_type=properties['ProductType'], os_version=properties['OSVersion'])) + return result + + +def get_remoted_device(udid: str, timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> RSDDevice: + devices = get_remoted_devices(timeout=timeout) + for device in devices: + if device.udid == udid: + return device + raise NoDeviceConnectedError() diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py new file mode 100644 index 000000000..ef4ce657d --- /dev/null +++ b/pymobiledevice3/remote/remotexpc.py @@ -0,0 +1,101 @@ +import socket +from socket import create_connection +from typing import Mapping, Optional, Tuple + +from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame, SettingsFrame, WindowUpdateFrame + +from pymobiledevice3.exceptions import StreamClosedError +from pymobiledevice3.remote.xpc_message import XpcWrapper, create_xpc_wrapper, get_object_from_xpc_wrapper + +# Extracted by sniffing `remoted` traffic via Wireshark +DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS = 100 +DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE = 1048576 +DEFAULT_WIN_SIZE_INCR = 983041 + +FRAME_HEADER_SIZE = 9 +HTTP2_MAGIC = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + + +class RemoteXPCConnection: + def __init__(self, address: Tuple[str, int]): + self.address = address + self.sock: Optional[socket.socket] = None + self.next_message_id = 0 + self.peer_info = None + + def __enter__(self) -> 'RemoteXPCConnection': + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + + def connect(self) -> None: + self.sock = create_connection(self.address) + self._do_handshake() + + def close(self) -> None: + self.sock.close() + + def send_request(self, data: Mapping) -> None: + self.sock.sendall( + DataFrame(stream_id=1, data=create_xpc_wrapper(data, message_id=self.next_message_id)).serialize()) + + def receive_response(self): + while True: + frame = self._receive_frame() + if isinstance(frame, GoAwayFrame): + raise StreamClosedError() + if not isinstance(frame, DataFrame): + continue + xpc_message = XpcWrapper.parse(frame.data).message + if xpc_message.payload is None: + continue + if xpc_message.payload.obj.data.entries is None: + continue + + self.next_message_id = xpc_message.message_id + 1 + return get_object_from_xpc_wrapper(frame.data) + + def send_receive_request(self, data: Mapping): + self.send_request(data) + return self.receive_response() + + def _do_handshake(self) -> None: + self.sock.sendall(HTTP2_MAGIC) + + # send h2 headers + self._send_frame(SettingsFrame(settings={ + SettingsFrame.MAX_CONCURRENT_STREAMS: DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS, + SettingsFrame.INITIAL_WINDOW_SIZE: DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE, + })) + self._send_frame(WindowUpdateFrame(stream_id=0, window_increment=DEFAULT_WIN_SIZE_INCR)) + self._send_frame(HeadersFrame(stream_id=1, flags=['END_HEADERS'])) + + # send first actual requests + self.send_request({}) + self._send_frame(DataFrame(stream_id=1, data=XpcWrapper.build({'size': 0, 'flags': 0x0201, 'payload': None}))) + self.next_message_id += 1 + self._send_frame(HeadersFrame(stream_id=3, flags=['END_HEADERS'])) + self._send_frame( + DataFrame(stream_id=3, data=XpcWrapper.build({'size': 0, 'flags': 0x00400001, 'payload': None}))) + self.next_message_id += 1 + + assert isinstance(self._receive_frame(), SettingsFrame) + + self._send_frame(SettingsFrame(flags=['ACK'])) + + def _send_frame(self, frame: Frame) -> None: + self.sock.sendall(frame.serialize()) + + def _receive_frame(self) -> Frame: + buf = self._recvall(FRAME_HEADER_SIZE) + frame, additional_size = Frame.parse_frame_header(memoryview(buf)) + frame.parse_body(memoryview(self._recvall(additional_size))) + return frame + + def _recvall(self, size: int) -> bytes: + buf = b'' + while len(buf) < size: + buf += self.sock.recv(size - len(buf)) + return buf diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py new file mode 100644 index 000000000..ef7ee60be --- /dev/null +++ b/pymobiledevice3/remote/xpc_message.py @@ -0,0 +1,267 @@ +import uuid +from typing import Any, List, Mapping + +from construct import Aligned, Array, Bytes, Const, CString, Default, Double, Enum, ExprAdapter, FlagsEnum, \ + GreedyBytes, Hex, If, Int32ul, Int64sl, Int64ul, LazyBound +from construct import Optional as ConstructOptional +from construct import Prefixed, Probe, Struct, Switch, this + +XpcMessageType = Enum(Hex(Int32ul), + NULL=0x00001000, + BOOL=0x00002000, + INT64=0x00003000, + UINT64=0x00004000, + DOUBLE=0x00005000, + POINTER=0x00006000, + DATE=0x00007000, + DATA=0x00008000, + STRING=0x00009000, + UUID=0x0000a000, + FD=0x0000b000, + SHMEM=0x0000c000, + MACH_SEND=0x0000d000, + ARRAY=0x0000e000, + DICTIONARY=0x0000f000, + ERROR=0x00010000, + CONNECTION=0x00011000, + ENDPOINT=0x00012000, + SERIALIZER=0x00013000, + PIPE=0x00014000, + MACH_RECV=0x00015000, + BUNDLE=0x00016000, + SERVICE=0x00017000, + SERVICE_INSTANCE=0x00018000, + ACTIVITY=0x00019000, + FILE_TRANSFER=0x0001a000, + ) +XpcFlags = FlagsEnum(Hex(Int32ul), + ALWAYS_SET=0x00000001, + DATA_PRESENT=0x00000100, + HEARTBEAT_REQUEST=0x00010000, + HEARTBEAT_RESPONSE=0x00020000, + FILE_TX_STREAM_REQUEST=0x00100000, + FILE_TX_STREAM_RESPONSE=0x00200000, + INIT_HANDSHAKE=0x00400000, + ) +AlignedString = Aligned(4, CString('utf8')) +XpcNull = None +XpcBool = Int32ul +XpcInt64 = Int64sl +XpcUInt64 = Int64ul +XpcDouble = Double +XpcPointer = None +XpcDate = Int64ul +XpcData = Aligned(4, Prefixed(Int32ul, GreedyBytes)) +XpcString = Aligned(4, Prefixed(Int32ul, CString('utf8'))) +XpcUuid = Bytes(16) +XpcFd = Int32ul +XpcShmem = Struct('length' / Int32ul, Int32ul) +XpcArray = Prefixed(Int32ul, LazyBound(lambda: XpcObject)) +XpcDictionaryEntry = Struct( + 'key' / AlignedString, + 'value' / LazyBound(lambda: XpcObject), +) +XpcDictionary = Prefixed(Int32ul, Struct( + 'count' / Hex(Int32ul), + 'entries' / If(this.count > 0, Array(this.count, XpcDictionaryEntry)), +)) +XpcObject = Struct( + 'type' / XpcMessageType, + 'data' / Switch(this.type, { + XpcMessageType.DICTIONARY: XpcDictionary, + XpcMessageType.STRING: XpcString, + XpcMessageType.INT64: XpcInt64, + XpcMessageType.UINT64: XpcUInt64, + XpcMessageType.DOUBLE: XpcDouble, + XpcMessageType.BOOL: XpcBool, + XpcMessageType.NULL: XpcNull, + XpcMessageType.UUID: XpcUuid, + XpcMessageType.POINTER: XpcPointer, + XpcMessageType.DATE: XpcDate, + XpcMessageType.DATA: XpcData, + XpcMessageType.FD: XpcFd, + XpcMessageType.SHMEM: XpcShmem, + XpcMessageType.ARRAY: XpcArray, + }, default=Probe(lookahead=20)), +) +XpcPayload = Struct( + 'magic' / Hex(Const(0x42133742, Int32ul)), + 'protocol_version' / Hex(Const(0x00000005, Int32ul)), + 'obj' / XpcObject, +) +XpcWrapper = Struct( + 'magic' / Hex(Const(0x29b00b92, Int32ul)), + 'flags' / Default(XpcFlags, XpcFlags.ALWAYS_SET), + 'message' / Prefixed( + ExprAdapter(Int64ul, lambda obj, context: obj + 8, lambda obj, context: obj - 8), + Struct( + 'message_id' / Hex(Default(Int64ul, 0)), + 'payload' / ConstructOptional(XpcPayload), + )) +) + + +class XpcInt64Type(int): + pass + + +class XpcUInt64Type(int): + pass + + +def _decode_xpc_dictionary(xpc_object) -> Mapping: + if xpc_object.data.count == 0: + return {} + result = {} + for entry in xpc_object.data.entries: + result[entry.key] = _decode_xpc_object(entry.value) + return result + + +def _decode_xpc_array(xpc_object) -> List: + result = [] + for entry in xpc_object.data.entries: + result.append(_decode_xpc_object(entry.value)) + return result + + +def _decode_xpc_bool(xpc_object) -> bool: + return bool(xpc_object.data) + + +def _decode_xpc_int64(xpc_object) -> XpcInt64Type: + return XpcInt64Type(xpc_object.data) + + +def _decode_xpc_uint64(xpc_object) -> XpcUInt64Type: + return XpcUInt64Type(xpc_object.data) + + +def _decode_xpc_uuid(xpc_object) -> uuid.UUID: + return uuid.UUID(bytes=xpc_object.data) + + +def _decode_xpc_string(xpc_object) -> str: + return xpc_object.data + + +def _decode_xpc_data(xpc_object) -> bytes: + return xpc_object.data + + +def _decode_xpc_object(xpc_object) -> Any: + decoders = { + XpcMessageType.DICTIONARY: _decode_xpc_dictionary, + XpcMessageType.ARRAY: _decode_xpc_array, + XpcMessageType.BOOL: _decode_xpc_bool, + XpcMessageType.INT64: _decode_xpc_int64, + XpcMessageType.UINT64: _decode_xpc_uint64, + XpcMessageType.UUID: _decode_xpc_uuid, + XpcMessageType.STRING: _decode_xpc_string, + XpcMessageType.DATA: _decode_xpc_data, + } + decoder = decoders.get(xpc_object.type) + if decoder is None: + raise TypeError(f'deserialize error: {xpc_object}') + return decoder(xpc_object) + + +def get_object_from_xpc_wrapper(payload: bytes): + payload = XpcWrapper.parse(payload).message.payload + if payload is None: + return None + return _decode_xpc_object(payload.obj) + + +def _build_xpc_array(payload: List) -> Mapping: + entries = [] + for entry in payload: + entry = _build_xpc_object(entry) + entries.append(entry) + return { + 'type': XpcMessageType.ARRAY, + 'data': entries + } + + +def _build_xpc_dictionary(payload: Mapping) -> Mapping: + entries = [] + for key, value in payload.items(): + entry = {'key': key, 'value': _build_xpc_object(value)} + entries.append(entry) + return { + 'type': XpcMessageType.DICTIONARY, + 'data': { + 'count': len(entries), + 'entries': entries, + } + } + + +def _build_xpc_bool(payload: bool) -> Mapping: + return { + 'type': XpcMessageType.BOOL, + 'data': payload, + } + + +def _build_xpc_string(payload: str) -> Mapping: + return { + 'type': XpcMessageType.STRING, + 'data': payload, + } + + +def _build_xpc_data(payload: bool) -> Mapping: + return { + 'type': XpcMessageType.DATA, + 'data': payload, + } + + +def _build_xpc_uint64(payload: XpcUInt64Type) -> Mapping: + return { + 'type': XpcMessageType.UINT64, + 'data': payload, + } + + +def _build_xpc_int64(payload: XpcInt64Type) -> Mapping: + return { + 'type': XpcMessageType.INT64, + 'data': payload, + } + + +def _build_xpc_object(payload: Any) -> Mapping: + payload_builders = { + list: _build_xpc_array, + dict: _build_xpc_dictionary, + bool: _build_xpc_bool, + str: _build_xpc_string, + bytes: _build_xpc_data, + bytearray: _build_xpc_data, + 'XpcUInt64Type': _build_xpc_uint64, + 'XpcInt64Type': _build_xpc_int64, + } + builder = payload_builders.get(type(payload), payload_builders.get(type(payload).__name__)) + if builder is None: + raise TypeError(f'unrecognized type for: {payload} {type(payload)}') + return builder(payload) + + +def create_xpc_wrapper(d: Mapping, message_id: int = 0) -> bytes: + flags = XpcFlags.ALWAYS_SET + if len(d.keys()) > 0: + flags |= XpcFlags.DATA_PRESENT + + xpc_payload = { + 'message_id': message_id, + 'payload': {'obj': _build_xpc_object(d)} + } + + xpc_wrapper = { + 'flags': flags, + 'message': xpc_payload + } + return XpcWrapper.build(xpc_wrapper) diff --git a/requirements.txt b/requirements.txt index 556c17d12..57a1b3973 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,4 @@ ipsw_parser>=1.1.2 remotezip zeroconf ifaddr +hyperframe From 0f78c46119ef60569b09e1baaa2024f4f6162f36 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 9 Jul 2023 14:09:54 +0300 Subject: [PATCH 104/234] remote: add `CoreDeviceTunnelService` --- pymobiledevice3/exceptions.py | 5 + .../remote/core_device_tunnel_service.py | 463 ++++++++++++++++++ requirements.txt | 2 + 3 files changed, 470 insertions(+) create mode 100644 pymobiledevice3/remote/core_device_tunnel_service.py diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index b394a0f50..d0ecf9e87 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -141,6 +141,11 @@ class ConnectionTerminatedError(PyMobileDevice3Exception): pass +class StreamClosedError(ConnectionTerminatedError): + """ Raise when trying to send a message on a closed stream. """ + pass + + class WebInspectorNotEnabledError(PyMobileDevice3Exception): """ Raise when Web Inspector is not enabled. """ pass diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py new file mode 100644 index 000000000..ec6c609e9 --- /dev/null +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -0,0 +1,463 @@ +import asyncio +import base64 +import binascii +import hashlib +import json +import logging +import platform +import plistlib +from collections import namedtuple +from pathlib import Path +from ssl import VerifyMode +from typing import List, Mapping, Optional, TextIO + +from aioquic.asyncio import QuicConnectionProtocol +from aioquic.asyncio.client import connect as aioquic_connect +from aioquic.asyncio.protocol import QuicStreamHandler +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.connection import QuicConnection +from aioquic.quic.events import QuicEvent, StreamDataReceived +from construct import Const, Container, Enum, GreedyBytes, GreedyRange, Int8ul, Int16ub, Int64ul, Prefixed, Struct +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives._serialization import Encoding, PublicFormat +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from srptools import SRPClientSession, SRPContext +from srptools.constants import PRIME_3072, PRIME_3072_GEN + +from pymobiledevice3.ca import make_cert +from pymobiledevice3.pair_records import create_pairing_records_cache_folder, generate_host_id +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type + +PairingDataComponentType = Enum(Int8ul, + METHOD=0x00, + IDENTIFIER=0x01, + SALT=0x02, + PUBLIC_KEY=0x03, + PROOF=0x04, + ENCRYPTED_DATA=0x05, + STATE=0x06, + ERROR=0x07, + RETRY_DELAY=0x08, + CERTIFICATE=0x09, + SIGNATURE=0x0a, + PERMISSIONS=0x0b, + FRAGMENT_DATA=0x0c, + FRAGMENT_LAST=0x0d, + SESSION_ID=0x0e, + TTL=0x0f, + EXTRA_DATA=0x10, + INFO=0x11, + ACL=0x12, + FLAGS=0x13, + VALIDATION_DATA=0x14, + MFI_AUTH_TOKEN=0x15, + MFI_PRODUCT_TYPE=0x16, + SERIAL_NUMBER=0x17, + MFI_AUTH_TOKEN_UUID=0x18, + APP_FLAGS=0x19, + OWNERSHIP_PROOF=0x1a, + SETUP_CODE_TYPE=0x1b, + PRODUCTION_DATA=0x1c, + APP_INFO=0x1d, + SEPARATOR=0xff) + +PairingDataComponentTLV8 = Struct( + 'type' / PairingDataComponentType, + 'data' / Prefixed(Int8ul, GreedyBytes), +) + +PairingDataComponentTLVBuf = GreedyRange(PairingDataComponentTLV8) + +PairConsentResult = namedtuple('PairConsentResult', 'public_key salt') + +CDTunnelPacket = Struct( + 'magic' / Const(b'CDTunnel'), + 'body' / Prefixed(Int16ub, GreedyBytes), +) + + +class RemotePairingTunnel(QuicConnectionProtocol): + MTU = 1420 + + def __init__(self, quic: QuicConnection, stream_handler: Optional[QuicStreamHandler] = None): + super().__init__(quic, stream_handler) + self._queue = asyncio.Queue() + + async def request_tunnel_establish(self) -> Mapping: + stream_id = self._quic.get_next_available_stream_id() + # TODO: understand what this buffer should actually do + self._quic.send_datagram_frame(b'x' * 1024) + self._quic.send_stream_data(stream_id, self._encode_cdtunnel_packet( + {'type': 'clientHandshakeRequest', 'mtu': self.MTU})) + self.transmit() + return await self._queue.get() + + def quic_event_received(self, event: QuicEvent) -> None: + if isinstance(event, StreamDataReceived): + self._queue.put_nowait(json.loads(CDTunnelPacket.parse(event.data).body)) + + @staticmethod + def _encode_cdtunnel_packet(data: Mapping) -> bytes: + return CDTunnelPacket.build({'body': json.dumps(data).encode()}) + + +class CoreDeviceTunnelService: + WIRE_PROTOCOL_VERSION = 19 + + def __init__(self, rsd: RemoteServiceDiscoveryService): + self._logger = logging.getLogger(__name__) + self._sequence_number = 0 + self._encrypted_sequence_number = 0 + self.rsd = rsd + self.service = None + self.version = None + self.handshake_info = None + self.x25519_private_key = X25519PrivateKey.generate() + self.ed25519_private_key = Ed25519PrivateKey.generate() + self.identifier = generate_host_id() + self.srp_context = None + self.encryption_key = None + self.signature = None + + def connect(self, autopair: bool = True) -> None: + self.service = self.rsd.connect_to_service('com.apple.internal.dt.coredevice.untrusted.tunnelservice') + self.version = self.service.receive_response()['ServiceVersion'] + + self._attempt_pair_verify() + if not self._validate_pairing(): + if autopair: + self._pair() + self._init_client_server_main_encryption_keys() + + def create_listener(self, private_key: RSAPrivateKey, protocol: str = 'quic') -> Mapping: + request = {'request': {'_0': {'createListener': { + 'key': base64.b64encode( + private_key.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + ).decode(), + 'transportProtocolType': protocol}}}} + + response = self._send_receive_encrypted_request(request) + return response['createListener'] + + async def start_quic_tunnel(self, private_key: RSAPrivateKey, secrets_log_file: Optional[TextIO] = None) -> Mapping: + parameters = self.create_listener(private_key, protocol='quic') + cert = make_cert(private_key, private_key.public_key()) + configuration = QuicConfiguration(alpn_protocols=['RemotePairingTunnelProtocol'], + is_client=True, + certificate=cert, + private_key=private_key, + verify_mode=VerifyMode.CERT_NONE) + configuration.secrets_log_file = secrets_log_file + + host = self.service.address[0] + port = parameters['port'] + + self._logger.debug(f'Connecting to {host}:{port}') + async with aioquic_connect( + host, + port, + configuration=configuration, + create_protocol=RemotePairingTunnel, + ) as client: + self._logger.debug('quic connected') + return await client.request_tunnel_establish() + + def save_pair_record(self) -> None: + self.pair_record_path.write_bytes( + plistlib.dumps({ + 'public_key': self.ed25519_private_key.public_key().public_bytes_raw(), + 'private_key': self.ed25519_private_key.private_bytes_raw(), + })) + + @property + def pair_record(self) -> Optional[Mapping]: + if self.pair_record_path.exists(): + return plistlib.loads(self.pair_record_path.read_bytes()) + return None + + @property + def pair_record_path(self) -> Path: + pair_records_cache_directory = create_pairing_records_cache_folder() + return pair_records_cache_directory / f'remote_{self.handshake_info["peerDeviceInfo"]["identifier"]}.plist' + + def _pair(self) -> None: + pairing_consent_result = self._request_pair_consent() + self._init_srp_context(pairing_consent_result) + self._verify_proof() + self._save_pair_record_on_peer() + self._init_client_server_main_encryption_keys() + self._create_remote_unlock() + self.save_pair_record() + + def _request_pair_consent(self) -> PairConsentResult: + """ Display a Trust / Don't Trust dialog """ + + tlv = PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.METHOD, 'data': b'\x00'}, + {'type': PairingDataComponentType.STATE, 'data': b'\x01'}, + ]) + + self._send_pairing_data({'data': tlv, + 'kind': 'setupManualPairing', + 'sendingHost': platform.node(), + 'startNewSession': True}) + assert 'awaitingUserConsent' in self._receive_plain_response()['event']['_0'] + response = self._receive_pairing_data() + data = self.decode_tlv(PairingDataComponentTLVBuf.parse( + response)) + return PairConsentResult(public_key=data[PairingDataComponentType.PUBLIC_KEY], + salt=data[PairingDataComponentType.SALT]) + + def _init_srp_context(self, pairing_consent_result: PairConsentResult) -> None: + # Receive server public and salt and process them. + client_session = SRPClientSession( + SRPContext('Pair-Setup', password='000000', prime=PRIME_3072, generator=PRIME_3072_GEN, + hash_func=hashlib.sha512)) + client_session.process(pairing_consent_result.public_key.hex(), + pairing_consent_result.salt.hex()) + self.srp_context = client_session + self.encryption_key = binascii.unhexlify(self.srp_context.key) + + def _verify_proof(self) -> None: + client_public = binascii.unhexlify(self.srp_context.public) + client_session_key_proof = binascii.unhexlify(self.srp_context.key_proof) + + tlv = PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.STATE, 'data': b'\x03'}, + {'type': PairingDataComponentType.PUBLIC_KEY, 'data': client_public[:255]}, + {'type': PairingDataComponentType.PUBLIC_KEY, 'data': client_public[255:]}, + {'type': PairingDataComponentType.PROOF, 'data': client_session_key_proof}, + ]) + + response = self._send_receive_pairing_data({ + 'data': tlv, + 'kind': 'setupManualPairing', + 'sendingHost': platform.node(), + 'startNewSession': False}) + data = self.decode_tlv(PairingDataComponentTLVBuf.parse(response)) + assert self.srp_context.verify_proof(data[PairingDataComponentType.PROOF].hex().encode()) + + def _save_pair_record_on_peer(self) -> Mapping: + # HKDF with above computed key (SRP_compute_key) + Pair-Setup-Encrypt-Salt + Pair-Setup-Encrypt-Info + # result used as key for chacha20-poly1305 + setup_encryption_key = HKDF( + algorithm=hashes.SHA512(), + length=32, + salt=b'Pair-Setup-Encrypt-Salt', + info=b'Pair-Setup-Encrypt-Info', + ).derive(self.encryption_key) + + self.ed25519_private_key = Ed25519PrivateKey.generate() + + # HKDF with above computed key: + # (SRP_compute_key) + Pair-Setup-Controller-Sign-Salt + Pair-Setup-Controller-Sign-Info + signbuf = HKDF( + algorithm=hashes.SHA512(), + length=32, + salt=b'Pair-Setup-Controller-Sign-Salt', + info=b'Pair-Setup-Controller-Sign-Info', + ).derive(self.encryption_key) + + signbuf += self.identifier.encode() + signbuf += self.ed25519_private_key.public_key().public_bytes_raw() + + self.signature = self.ed25519_private_key.sign(signbuf) + + # TODO: use opack when its done + device_info = b'\xe7FaltIRK\x80\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{FbtAddrQ11:22:33:44:55:66Cmacv\x14\x98wi' \ + b'\rq[remotepairing_serial_numberLAAAAAAAAAAAAIaccountIDa$11111111-1111-1111-1111-111111111111' \ + b'EmodelJMacmini9,1DnameQUser\xe2\x80\x99s Mac mini' + + tlv = PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.IDENTIFIER, 'data': self.identifier.encode()}, + {'type': PairingDataComponentType.PUBLIC_KEY, + 'data': self.ed25519_private_key.public_key().public_bytes_raw()}, + {'type': PairingDataComponentType.SIGNATURE, 'data': self.signature}, + {'type': PairingDataComponentType.INFO, 'data': device_info}, + ]) + + cip = ChaCha20Poly1305(setup_encryption_key) + encrypted_data = cip.encrypt(b'\x00\x00\x00\x00PS-Msg05', tlv, b'') + + tlv = PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.ENCRYPTED_DATA, 'data': encrypted_data[:255]}, + {'type': PairingDataComponentType.ENCRYPTED_DATA, 'data': encrypted_data[255:]}, + {'type': PairingDataComponentType.STATE, 'data': b'\x05'}, + ]) + + response = self._send_receive_pairing_data({ + 'data': tlv, + 'kind': 'setupManualPairing', + 'sendingHost': platform.node(), + 'startNewSession': False}) + data = self.decode_tlv(PairingDataComponentTLVBuf.parse(response)) + + tlv = PairingDataComponentTLVBuf.parse(cip.decrypt( + b'\x00\x00\x00\x00PS-Msg06', data[PairingDataComponentType.ENCRYPTED_DATA], b'')) + + return tlv + + def _init_client_server_main_encryption_keys(self) -> None: + client_key = HKDF( + algorithm=hashes.SHA512(), + length=32, + salt=None, + info=b'ClientEncrypt-main', + ).derive(self.encryption_key) + self.client_cip = ChaCha20Poly1305(client_key) + + server_key = HKDF( + algorithm=hashes.SHA512(), + length=32, + salt=None, + info=b'ServerEncrypt-main', + ).derive(self.encryption_key) + self.server_cip = ChaCha20Poly1305(server_key) + + def _create_remote_unlock(self) -> None: + response = self._send_receive_encrypted_request({'request': {'_0': {'createRemoteUnlockKey': {}}}}) + self.remote_unlock_host_key = response['createRemoteUnlockKey']['hostKey'] + + def _attempt_pair_verify(self) -> None: + self.handshake_info = self._send_receive_handshake({ + 'hostOptions': {'attemptPairVerify': True}, + 'wireProtocolVersion': XpcInt64Type(self.WIRE_PROTOCOL_VERSION)}) + + def _validate_pairing(self) -> bool: + pairing_data = PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.STATE, 'data': b'\x01'}, + {'type': PairingDataComponentType.PUBLIC_KEY, + 'data': self.x25519_private_key.public_key().public_bytes_raw()}, + ]) + response = self._send_receive_pairing_data({'data': pairing_data, + 'kind': 'verifyManualPairing', + 'startNewSession': True}) + + data = self.decode_tlv(PairingDataComponentTLVBuf.parse(response)) + peer_public_key = X25519PublicKey.from_public_bytes(data[PairingDataComponentType.PUBLIC_KEY]) + self.encryption_key = self.x25519_private_key.exchange(peer_public_key) + + derived_key = HKDF( + algorithm=hashes.SHA512(), + length=32, + salt=b'Pair-Verify-Encrypt-Salt', + info=b'Pair-Verify-Encrypt-Info', + ).derive(self.encryption_key) + cip = ChaCha20Poly1305(derived_key) + + # TODO: + # we should be able to verify from the received encrypted data, but from some reason we failed to + # do so. instead, we verify using the next stage + + if self.pair_record is None: + private_key = Ed25519PrivateKey.from_private_bytes(b'\x00' * 0x20) + else: + private_key = Ed25519PrivateKey.from_private_bytes(self.pair_record['private_key']) + + signbuf = b'' + signbuf += self.x25519_private_key.public_key().public_bytes_raw() + signbuf += self.identifier.encode() + signbuf += peer_public_key.public_bytes_raw() + + signature = private_key.sign(signbuf) + + encrypted_data = cip.encrypt(b'\x00\x00\x00\x00PV-Msg03', PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.IDENTIFIER, 'data': self.identifier.encode()}, + {'type': PairingDataComponentType.SIGNATURE, 'data': signature}, + ]), b'') + + pairing_data = PairingDataComponentTLVBuf.build([ + {'type': PairingDataComponentType.STATE, 'data': b'\x03'}, + {'type': PairingDataComponentType.ENCRYPTED_DATA, 'data': encrypted_data}, + ]) + + response = self._send_receive_pairing_data({ + 'data': pairing_data, + 'kind': 'verifyManualPairing', + 'startNewSession': False}) + data = self.decode_tlv(PairingDataComponentTLVBuf.parse(response)) + + if PairingDataComponentType.ERROR in data: + self._send_pair_verify_failed() + return False + + return True + + def _send_pair_verify_failed(self) -> None: + self._send_plain_request({'event': {'_0': {'pairVerifyFailed': {}}}}) + + def _send_receive_encrypted_request(self, request: Mapping) -> Mapping: + nonce = Int64ul.build(self._encrypted_sequence_number) + b'\x00' * 4 + encrypted_data = self.client_cip.encrypt( + nonce, + json.dumps(request).encode(), + b'') + + response = self.service.send_receive_request({ + 'mangledTypeName': 'RemotePairing.ControlChannelMessageEnvelope', + 'value': {'message': { + 'streamEncrypted': {'_0': encrypted_data}}, + 'originatedBy': 'host', + 'sequenceNumber': XpcUInt64Type(self._sequence_number)}}) + self._encrypted_sequence_number += 1 + + encrypted_data = response['value']['message']['streamEncrypted']['_0'] + plaintext = self.server_cip.decrypt(nonce, encrypted_data, None) + return json.loads(plaintext)['response']['_1'] + + def _send_receive_handshake(self, handshake_data: Mapping) -> Mapping: + response = self._send_receive_plain_request({'request': {'_0': {'handshake': {'_0': handshake_data}}}}) + return response['response']['_1']['handshake']['_0'] + + def _send_receive_pairing_data(self, pairing_data: Mapping) -> Mapping: + self._send_pairing_data(pairing_data) + return self._receive_pairing_data() + + def _send_pairing_data(self, pairing_data: Mapping) -> None: + self._send_plain_request({'event': {'_0': {'pairingData': {'_0': pairing_data}}}}) + + def _receive_pairing_data(self) -> Mapping: + return self._receive_plain_response()['event']['_0']['pairingData']['_0']['data'] + + def _send_receive_plain_request(self, plain_request: Mapping): + self._send_plain_request(plain_request) + return self._receive_plain_response() + + def _send_plain_request(self, plain_request: Mapping) -> None: + self.service.send_request({ + 'mangledTypeName': 'RemotePairing.ControlChannelMessageEnvelope', + 'value': {'message': {'plain': {'_0': plain_request}}, + 'originatedBy': 'host', + 'sequenceNumber': XpcUInt64Type(self._sequence_number)}}) + self._sequence_number += 1 + + def _receive_plain_response(self) -> Mapping: + response = self.service.receive_response() + return response['value']['message']['plain']['_0'] + + @staticmethod + def decode_tlv(tlv_list: List[Container]) -> Mapping: + result = {} + for tlv in tlv_list: + if tlv.type in result: + result[tlv.type] += tlv.data + else: + result[tlv.type] = tlv.data + return result + + def __enter__(self) -> 'CoreDeviceTunnelService': + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.service.close() + + +def create_core_device_tunnel_service(rsd: RemoteServiceDiscoveryService, autopair: bool = True): + service = CoreDeviceTunnelService(rsd) + service.connect(autopair=autopair) + return service diff --git a/requirements.txt b/requirements.txt index 57a1b3973..fb1ab5104 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,5 @@ remotezip zeroconf ifaddr hyperframe +srptools +aioquic From d956becd113a066af0026acba489e051008ee48f Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 9 Jul 2023 14:10:48 +0300 Subject: [PATCH 105/234] cli: add `remote` subcommand --- pymobiledevice3/__main__.py | 6 ++- pymobiledevice3/cli/developer.py | 4 +- pymobiledevice3/cli/remote.py | 85 ++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 pymobiledevice3/cli/remote.py diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index c3aa7a4c1..a2e5f2976 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -23,6 +23,7 @@ from pymobiledevice3.cli.processes import cli as ps_cli from pymobiledevice3.cli.profile import cli as profile_cli from pymobiledevice3.cli.provision import cli as provision_cli +from pymobiledevice3.cli.remote import cli as remote_cli from pymobiledevice3.cli.restore import cli as restore_cli from pymobiledevice3.cli.springboard import cli as springboard_cli from pymobiledevice3.cli.syslog import cli as syslog_cli @@ -35,7 +36,9 @@ coloredlogs.install(level=logging.INFO) +logging.getLogger('quic').disabled = True logging.getLogger('asyncio').disabled = True +logging.getLogger('zeroconf').disabled = True logging.getLogger('parso.cache').disabled = True logging.getLogger('parso.cache.pickle').disabled = True logging.getLogger('parso.python.diff').disabled = True @@ -50,7 +53,8 @@ def cli(): cli_commands = click.CommandCollection(sources=[ developer_cli, mounter_cli, apps_cli, profile_cli, lockdown_cli, diagnostics_cli, syslog_cli, pcap_cli, crash_cli, afc_cli, ps_cli, notification_cli, usbmux_cli, power_assertion_cli, springboard_cli, - provision_cli, backup_cli, restore_cli, activation_cli, companion_cli, webinspector_cli, amfi_cli, bonjour_cli + provision_cli, backup_cli, restore_cli, activation_cli, companion_cli, webinspector_cli, amfi_cli, bonjour_cli, + remote_cli ]) cli_commands.context_settings = dict(help_option_names=['-h', '--help']) try: diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index f5da4f3f4..b2daf1b80 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -222,8 +222,8 @@ def netstat(lockdown: LockdownClient): for event in monitor: if isinstance(event, ConnectionDetectionEvent): logger.info( - f'Connection detected: {event.local_address.data.address}:{event.local_address.port} -> ' - f'{event.remote_address.data.address}:{event.remote_address.port}') + f'Connection detected: {event.local_address.data.hostname}:{event.local_address.port} -> ' + f'{event.remote_address.data.hostname}:{event.remote_address.port}') @dvt.command('screenshot', cls=Command) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py new file mode 100644 index 000000000..fbc31d5ef --- /dev/null +++ b/pymobiledevice3/cli/remote.py @@ -0,0 +1,85 @@ +import asyncio +import logging +import os +from typing import Optional + +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.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 + +logger = logging.getLogger(__name__) + + +class RemoteCommand(click.Command): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.params[:0] = [ + click.Option(('hostname', '--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), + ] + + @staticmethod + def udid(ctx, param: str, value: str) -> Optional[str]: + if '_PYMOBILEDEVICE3_COMPLETE' in os.environ: + # prevent lockdown connection establishment when in autocomplete mode + return + + if value is not None: + return get_remoted_device(udid=value).hostname + + device_options = get_remoted_devices() + if len(device_options) == 0: + raise NoDeviceConnectedError() + elif len(device_options) == 1: + return device_options[0].hostname + + return prompt_device_list(device_options).hostname + + +@click.group() +def cli(): + """ remote cli """ + pass + + +@cli.group('remote') +def remote_cli(): + """ remote options """ + pass + + +@remote_cli.command('rsd-info', cls=RemoteCommand) +@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) +@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): + """ start a remote listener """ + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd: + with create_core_device_tunnel_service(rsd, autopair=True) as service: + print_json(service.create_listener(private_key, protocol=protocol), colored=color) + + +@remote_cli.command('start-quic-tunnel', cls=RemoteCommand) +@click.option('--color/--no-color', default=True) +def start_quic_tunnel(hostname: str, color: bool): + """ start quic tunnel """ + logger.critical('This is a WIP command. Will only print the required parameters for the quic connection') + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + 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) From 14cc9e1a6d1499b08601bc355138cdc224c59934 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 9 Jul 2023 08:49:16 +0300 Subject: [PATCH 106/234] lockdown: add `create_using_remote()` --- pymobiledevice3/lockdown.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index d27248ff3..9380fb1c4 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -666,3 +666,35 @@ def create_using_tcp(hostname: str, identifier: str = None, label: str = DEFAULT pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, autopair=autopair, port=port, hostname=hostname) return client + + +def create_using_remote(hostname: str, identifier: str = None, label: str = DEFAULT_LABEL, autopair: bool = True, + pair_timeout: int = None, local_hostname: str = None, pair_record: Mapping = None, + pairing_records_cache_folder: Path = None, + port: int = SERVICE_PORT) -> TcpLockdownClient: + """ + Create a TcpLockdownClient instance over RSD + + :param hostname: The target device hostname + :param identifier: Used as an identifier to look for the device pair record + :param label: lockdownd user-agent + :param autopair: Attempt to pair with device (blocking) if not already paired + :param pair_timeout: Timeout for autopair + :param local_hostname: Used as a seed to generate the HostID + :param pair_record: Use this pair record instead of the default behavior (search in host/create our own) + :param pairing_records_cache_folder: Use the following location to search and save pair records + :param port: lockdownd service port + :return: TcpLockdownClient instance + """ + service = ServiceConnection.create_using_tcp(hostname, port) + service.send_plist({'Label': label, 'ProtocolVersion': '2', 'Request': 'RSDCheckin'}) + + # we expect two responses after the first request + service.recv_plist() + service.recv_plist() + + client = TcpLockdownClient.create( + service, identifier=identifier, label=label, local_hostname=local_hostname, pair_record=pair_record, + pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, autopair=autopair, + port=port, hostname=hostname) + return client From 37098e1a46a476964c635117dbf3092f9741340f Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 11 Jul 2023 20:18:49 +0300 Subject: [PATCH 107/234] requirements: use `developer_disk_image` to manage the `auto-mount` --- pymobiledevice3/cli/mounter.py | 94 +++---------------- pymobiledevice3/exceptions.py | 5 + .../services/mobile_image_mounter.py | 76 ++++++++++++++- requirements.txt | 1 + 4 files changed, 92 insertions(+), 84 deletions(-) diff --git a/pymobiledevice3/cli/mounter.py b/pymobiledevice3/cli/mounter.py index ccd222c58..4cee00b6f 100644 --- a/pymobiledevice3/cli/mounter.py +++ b/pymobiledevice3/cli/mounter.py @@ -1,24 +1,16 @@ -import json import logging from functools import update_wrapper from pathlib import Path -from typing import List from urllib.error import URLError -from urllib.request import urlopen import click -import requests -from tqdm import tqdm from pymobiledevice3.cli.cli_common import Command, print_json -from pymobiledevice3.common import get_home_folder -from pymobiledevice3.exceptions import AlreadyMountedError, NotMountedError, UnsupportedCommandError +from pymobiledevice3.exceptions import AlreadyMountedError, DeveloperDiskImageNotFoundError, NotMountedError, \ + UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.mobile_image_mounter import DeveloperDiskImageMounter, MobileImageMounterService, \ - PersonalizedImageMounter - -DISK_IMAGE_TREE = 'https://api.github.com/repos/pdso/DeveloperDiskImage/git/trees/master' -DEVELOPER_DISK_IMAGE_URL = 'https://github.com/pdso/DeveloperDiskImage/raw/master/{ios_version}/{file_name}' + PersonalizedImageMounter, auto_mount logger = logging.getLogger(__name__) @@ -97,27 +89,6 @@ def mounter_umount_personalized(lockdown: LockdownClient): logger.error('Personalized image isn\'t currently mounted') -def download_file(url, local_filename): - logger.debug(f'downloading: {local_filename}') - with requests.get(url, stream=True) as r: - r.raise_for_status() - total_size_in_bytes = int(r.headers.get('content-length', 0)) - - with tqdm(total=total_size_in_bytes, unit='iB', unit_scale=True, dynamic_ncols=True) as progress_bar: - with open(local_filename, 'wb') as f: - for chunk in r.iter_content(chunk_size=8192): - progress_bar.update(len(chunk)) - f.write(chunk) - - return local_filename - - -def get_all_versions() -> List[str]: - data = urlopen(DISK_IMAGE_TREE).read() - json_data = json.loads(data) - return [item.get('path') for item in json_data.get('tree')][0:-3] - - @mounter.command('mount-developer', cls=Command) @click.argument('image', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @click.argument('signature', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @@ -146,58 +117,19 @@ def mounter_mount_personalized(lockdown: LockdownClient, image: str, trust_cache 'connection') def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): """ auto-detect correct DeveloperDiskImage and mount it """ - image_type = 'Developer' - - if xcode is None: - # avoid "default"-ing this option, because Windows and Linux won't have this path - xcode = Path('/Applications/Xcode.app') - if not (xcode.exists()): - xcode = get_home_folder() / 'Xcode.app' - xcode.mkdir(parents=True, exist_ok=True) - - image_mounter = DeveloperDiskImageMounter(lockdown=lockdown) - if image_mounter.is_image_mounted(image_type): - logger.error('DeveloperDiskImage is already mounted') - return - - logger.debug('trying to figure out the best suited DeveloperDiskImage') - if version is None: - version = lockdown.sanitized_ios_version - image_dir = f'{xcode}/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/{version}' - image_path = f'{image_dir}/DeveloperDiskImage.dmg' - signature = f'{image_path}.signature' - developer_disk_image_dir = Path(image_path).parent - - image_path = Path(image_path) - signature = Path(signature) - - if not image_path.exists(): - try: - available_versions = get_all_versions() - if version not in available_versions: - logger.error( - f'Unable to find DeveloperDiskImage for {version}. available versions: {available_versions}') - return - except URLError: - logger.warning('failed to query DeveloperDiskImage versions') - try: - developer_disk_image_dir.mkdir(exist_ok=True) - - if not image_path.exists(): - download_file(DEVELOPER_DISK_IMAGE_URL.format(ios_version=version, file_name=image_path.name), image_path) - - if not signature.exists(): - download_file(DEVELOPER_DISK_IMAGE_URL.format(ios_version=version, file_name=signature.name), signature) - - except PermissionError: + auto_mount(lockdown, xcode=xcode, version=version) + logger.info('DeveloperDiskImage mounted successfully') + except URLError: + logger.warning('failed to query DeveloperDiskImage versions') + except DeveloperDiskImageNotFoundError: + logger.error('Unable to find the correct DeveloperDiskImage') + except AlreadyMountedError: + logger.error('DeveloperDiskImage already mounted') + except PermissionError as e: logger.error( - f'DeveloperDiskImage could not be saved to Xcode default path ({developer_disk_image_dir}). ' + f'DeveloperDiskImage could not be saved to Xcode default path ({e.filename}). ' f'Please make sure your user has the necessary permissions') - return - - image_mounter.mount(image_path, signature) - logger.info('DeveloperDiskImage mounted successfully') @mounter.command('query-developer-mode-status', cls=Command) diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index d0ecf9e87..83f063a24 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -192,6 +192,11 @@ class DeveloperModeIsNotEnabledError(PyMobileDevice3Exception): pass +class DeveloperDiskImageNotFoundError(PyMobileDevice3Exception): + """ Failed to locate the correct DeveloperDiskImage.dmg """ + pass + + class DeveloperModeError(PyMobileDevice3Exception): """ Raise when amfid failed to enable developer mode. """ pass diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index b1cab5876..a9f1d731f 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -3,9 +3,13 @@ from pathlib import Path from typing import List, Mapping -from pymobiledevice3.exceptions import AlreadyMountedError, DeveloperModeIsNotEnabledError, InternalError, \ - MessageNotSupportedError, MissingManifestError, NotMountedError, PyMobileDevice3Exception, \ - UnsupportedCommandError +from developer_disk_image.repo import DeveloperDiskImageRepository +from packaging.version import Version + +from pymobiledevice3.common import get_home_folder +from pymobiledevice3.exceptions import AlreadyMountedError, DeveloperDiskImageNotFoundError, \ + DeveloperModeIsNotEnabledError, InternalError, MessageNotSupportedError, MissingManifestError, NotMountedError, \ + PyMobileDevice3Exception, UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.restore.tss import TSSRequest from pymobiledevice3.services.base_service import BaseService @@ -277,3 +281,69 @@ def get_manifest_from_tss(self, build_manifest: Mapping) -> bytes: response = request.send_receive() return response['ApImg4Ticket'] + + +def auto_mount_developer(lockdown: LockdownClient, xcode: str = None, version: str = None) -> None: + """ auto-detect correct DeveloperDiskImage and mount it """ + if xcode is None: + # avoid "default"-ing this option, because Windows and Linux won't have this path + xcode = Path('/Applications/Xcode.app') + if not (xcode.exists()): + xcode = get_home_folder() / 'Xcode.app' + xcode.mkdir(parents=True, exist_ok=True) + + image_mounter = DeveloperDiskImageMounter(lockdown=lockdown) + if image_mounter.is_image_mounted('Developer'): + raise AlreadyMountedError() + + if version is None: + version = lockdown.sanitized_ios_version + image_dir = f'{xcode}/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/{version}' + image_path = f'{image_dir}/DeveloperDiskImage.dmg' + signature = f'{image_path}.signature' + developer_disk_image_dir = Path(image_path).parent + + image_path = Path(image_path) + signature = Path(signature) + + if not image_path.exists(): + # download the DeveloperDiskImage from our repository + repo = DeveloperDiskImageRepository.create() + developer_disk_image = repo.get_developer_disk_image(version) + + if developer_disk_image is None: + raise DeveloperDiskImageNotFoundError() + + # write it filesystem + developer_disk_image_dir.mkdir(exist_ok=True, parents=True) + image_path.write_bytes(developer_disk_image.image) + signature.write_bytes(developer_disk_image.signature) + + image_mounter.mount(image_path, signature) + + +def auto_mount_personalized(lockdown: LockdownClient) -> None: + local_path = get_home_folder() / 'Xcode_iOS_DDI_Personalized' + local_path.mkdir(parents=True, exist_ok=True) + + image = local_path / 'Image.dmg' + build_manifest = local_path / 'BuildManifest.plist' + trustcache = local_path / 'Image.trustcache' + + if not image.exists(): + # download the Personalized image from our repository + repo = DeveloperDiskImageRepository.create() + personalized_image = repo.get_personalized_disk_image() + + image.write_bytes(personalized_image.image) + build_manifest.write_bytes(personalized_image.build_manifest) + trustcache.write_bytes(personalized_image.trustcache) + + PersonalizedImageMounter(lockdown=lockdown).mount(image, build_manifest, trustcache) + + +def auto_mount(lockdown: LockdownClient, xcode: str = None, version: str = None) -> None: + if Version(lockdown.product_version) < Version('17.0'): + auto_mount_developer(lockdown, xcode=xcode, version=version) + else: + auto_mount_personalized(lockdown) diff --git a/requirements.txt b/requirements.txt index fb1ab5104..e6801a979 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,3 +34,4 @@ ifaddr hyperframe srptools aioquic +developer_disk_image>=0.0.2 From 91dbe857dfe89950e94ce23cb4ee11ca893ac45b Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 12 Jul 2023 08:35:51 +0300 Subject: [PATCH 108/234] pyproject: bump version to 2.0.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9d437a51f..490738e81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.0.2" +version = "2.0.3" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 6ad90316062b8490413288066d72403e4272afc5 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 12 Jul 2023 15:29:53 +0300 Subject: [PATCH 109/234] xpc_message: fix xpc_array serialize/deserialize --- pymobiledevice3/remote/xpc_message.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index ef7ee60be..9fa9590a4 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -56,7 +56,9 @@ XpcUuid = Bytes(16) XpcFd = Int32ul XpcShmem = Struct('length' / Int32ul, Int32ul) -XpcArray = Prefixed(Int32ul, LazyBound(lambda: XpcObject)) +XpcArray = Prefixed(Int32ul, Struct( + 'count' / Int32ul, + 'entries' / Array(this.count, LazyBound(lambda: XpcObject)))) XpcDictionaryEntry = Struct( 'key' / AlignedString, 'value' / LazyBound(lambda: XpcObject), @@ -82,7 +84,7 @@ XpcMessageType.FD: XpcFd, XpcMessageType.SHMEM: XpcShmem, XpcMessageType.ARRAY: XpcArray, - }, default=Probe(lookahead=20)), + }, default=Probe(lookahead=1000)), ) XpcPayload = Struct( 'magic' / Hex(Const(0x42133742, Int32ul)), @@ -121,7 +123,7 @@ def _decode_xpc_dictionary(xpc_object) -> Mapping: def _decode_xpc_array(xpc_object) -> List: result = [] for entry in xpc_object.data.entries: - result.append(_decode_xpc_object(entry.value)) + result.append(_decode_xpc_object(entry)) return result From 5b3aa5507284320a00c3cf734d77c86b64508f54 Mon Sep 17 00:00:00 2001 From: Lori Witt Date: Wed, 12 Jul 2023 13:22:43 +0300 Subject: [PATCH 110/234] core_device_tunnel: use python opack library for creating opack buffer --- .../remote/core_device_tunnel_service.py | 14 ++++++++++---- requirements.txt | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index ec6c609e9..f4985d87d 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -25,6 +25,7 @@ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from opack import dumps from srptools import SRPClientSession, SRPContext from srptools.constants import PRIME_3072, PRIME_3072_GEN @@ -268,10 +269,15 @@ def _save_pair_record_on_peer(self) -> Mapping: self.signature = self.ed25519_private_key.sign(signbuf) - # TODO: use opack when its done - device_info = b'\xe7FaltIRK\x80\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{FbtAddrQ11:22:33:44:55:66Cmacv\x14\x98wi' \ - b'\rq[remotepairing_serial_numberLAAAAAAAAAAAAIaccountIDa$11111111-1111-1111-1111-111111111111' \ - b'EmodelJMacmini9,1DnameQUser\xe2\x80\x99s Mac mini' + device_info = dumps({ + 'altIRK': b'\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{', + 'btAddr': '11:22:33:44:55:66', + 'mac': b'\x11\x22\x33\x44\x55\x66', + 'remotepairing_serial_number': 'AAAAAAAAAAAA', + 'accountID': self.identifier, + 'model': 'computer-model', + 'name': platform.node() + }) tlv = PairingDataComponentTLVBuf.build([ {'type': PairingDataComponentType.IDENTIFIER, 'data': self.identifier.encode()}, diff --git a/requirements.txt b/requirements.txt index e6801a979..a3ff64d85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,4 @@ hyperframe srptools aioquic developer_disk_image>=0.0.2 +opack From 5b2710cbbb6557949eda18b610cf314ab71a827f Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 11 Jul 2023 09:36:50 +0300 Subject: [PATCH 111/234] remote: add `sniffer.py` --- pymobiledevice3/remote/sniffer.py | 205 ++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 pymobiledevice3/remote/sniffer.py diff --git a/pymobiledevice3/remote/sniffer.py b/pymobiledevice3/remote/sniffer.py new file mode 100644 index 000000000..8e506cd7f --- /dev/null +++ b/pymobiledevice3/remote/sniffer.py @@ -0,0 +1,205 @@ +import logging +from pprint import pformat +from typing import List, MutableMapping, Optional + +import click +import coloredlogs +from construct import ConstError, StreamError +from hexdump import hexdump +from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame +from remotexpc import HTTP2_MAGIC +from scapy.layers.inet import IP, TCP +from scapy.layers.inet6 import IPv6 +from scapy.packet import Packet +from scapy.sendrecv import sniff + +from pymobiledevice3.remote.core_device_tunnel_service import PairingDataComponentTLVBuf +from pymobiledevice3.remote.xpc_message import get_object_from_xpc_wrapper + +logger = logging.getLogger() + +coloredlogs.install(level=logging.DEBUG) + +FRAME_HEADER_SIZE = 9 + + +def create_stream_key(src: str, sport: int, dst: str, dport: int) -> str: + return f'{src}/{sport}//{dst}/{dport}' + + +class TCPStream: + def __init__(self, src: str, sport: int, dst: str, dport: int): + self.src = src + self.sport = sport + self.dst = dst + self.dport = dport + self.key = create_stream_key(src, sport, dst, dport) + self.data = bytearray() + self.seq: Optional[int] = None # so we know seq hasn't been initialized yet + self.segments = {} # data segments to add later + + def __repr__(self) -> str: + return f'Stream<{self.key}>' + + def __len__(self) -> int: + return len(self.data) + + def add(self, tcp_pkt: TCP) -> bool: + """ + Returns True if we added an in-order segment, False if not + """ + if self.seq is None: + # set initial seq + self.seq = tcp_pkt.seq + data = bytes(tcp_pkt.payload) + data_len = len(data) + seq_offset = tcp_pkt.seq - self.seq + if len(self.data) < seq_offset: + # if this data is out of order and needs to be inserted later + self.segments[seq_offset] = data + return False + else: + # if this data is in order (has a place to be inserted) + self.data[seq_offset:seq_offset + data_len] = data + # check if there are any waiting data segments to add + for seq_offset in sorted(self.segments.keys()): + if seq_offset <= len(self.data): # if we can add this segment to the stream + segment_payload = self.segments[seq_offset] + self.data[seq_offset:seq_offset + len(segment_payload)] = segment_payload + self.segments.pop(seq_offset) + else: + break # short circuit because list is sorted + return True + + +class H2Stream(TCPStream): + def pop_frames(self) -> List[Frame]: + """ Pop all available H2Frames """ + + # If self.data starts with the http/2 magic bytes, pop them off + if self.data.startswith(HTTP2_MAGIC): + logger.debug('HTTP/2 magic bytes') + self.data = self.data[len(HTTP2_MAGIC):] + self.seq += len(HTTP2_MAGIC) + + frames = [] + while len(self.data) >= FRAME_HEADER_SIZE: + frame, additional_size = Frame.parse_frame_header(memoryview(self.data[:FRAME_HEADER_SIZE])) + if len(self.data) - FRAME_HEADER_SIZE < additional_size: + # the frame has an incomplete body + break + self.data = self.data[FRAME_HEADER_SIZE:] + frame.parse_body(memoryview(self.data[:additional_size])) + self.data = self.data[additional_size:] + self.seq += FRAME_HEADER_SIZE + additional_size + frames.append(frame) + return frames + + +class RemoteXPCSniffer: + def __init__(self): + self._h2_streams: MutableMapping[str, H2Stream] = {} + self._previous_frame_data: MutableMapping[str, bytes] = {} + + def process_packet(self, packet: Packet) -> None: + if packet.haslayer(TCP) and packet[TCP].payload: + self._process_tcp(packet) + + def _process_tcp(self, pkt: Packet) -> None: + # we are going to separate TCP packets into TCP streams between unique + # endpoints (ip/port) then, for each stream, we will create a new H2Stream + # object and pass TCP packets into it H2Stream objects will take the bytes + # from each TCP packet and add them to the stream. No error correction / + # checksum checking will be done. The stream will just overwrite its bytes + # with whatever is presented in the packets. If the stream receives packets + # out of order, it will add the bytes at the proper index. + if pkt.haslayer(IP): + net_pkt = pkt[IP] + elif pkt.haslayer(IPv6): + net_pkt = pkt[IPv6] + else: + return + tcp_pkt = pkt[TCP] + stream_key = create_stream_key(net_pkt.src, tcp_pkt.sport, net_pkt.dst, tcp_pkt.dport) + stream = self._h2_streams.setdefault( + stream_key, H2Stream(net_pkt.src, tcp_pkt.sport, net_pkt.dst, tcp_pkt.dport)) + stream_finished_assembling = stream.add(tcp_pkt) + if stream_finished_assembling: # if we just added something in order + self._process_stream(stream) + + def _handle_data_frame(self, stream: H2Stream, frame: DataFrame) -> None: + previous_frame_data = self._previous_frame_data.get(stream.key, b'') + try: + xpc_message = get_object_from_xpc_wrapper(previous_frame_data + frame.data) + except ConstError: # if we don't know what this payload is + logger.debug( + f'New Data frame {stream.src}->{stream.dst} on HTTP/2 stream {frame.stream_id} TCP port {stream.dport}') + hexdump(frame.data[:64]) + if len(frame.data) > 64: + logger.debug(f'... {len(frame.data)} bytes') + return + except StreamError: + self._previous_frame_data[stream.key] = previous_frame_data + frame.data + return + + if stream.key in self._previous_frame_data: + self._previous_frame_data.pop(stream.key) + + if xpc_message is None: + return + + logger.info(f'As Python Object: {pformat(xpc_message)}') + + # print `pairingData` if exists, since it contains an inner struct + if 'value' not in xpc_message: + return + message = xpc_message['value']['message'] + if 'plain' not in message: + return + plain = message['plain']['_0'] + if 'event' not in plain: + return + pairing_data = plain['event']['_0']['pairingData']['_0']['data'] + logger.info(PairingDataComponentTLVBuf.parse(pairing_data)) + + def _handle_single_frame(self, stream: H2Stream, frame: Frame) -> None: + logger.debug(f'New HTTP/2 frame: {stream.key} ({frame})') + if isinstance(frame, HeadersFrame): + logger.debug( + f'{stream.src} opening stream {frame.stream_id} for communication on port {stream.dport}') + elif isinstance(frame, GoAwayFrame): + logger.debug(f'{stream.src} closing stream {frame.stream_id} on port {stream.sport}') + elif isinstance(frame, DataFrame): + self._handle_data_frame(stream, frame) + + def _process_stream(self, stream: H2Stream) -> None: + for frame in stream.pop_frames(): + self._handle_single_frame(stream, frame) + + +@click.group() +def cli(): + """ Parse RemoteXPC traffic """ + pass + + +@cli.command() +@click.argument('file', type=click.Path(exists=True, file_okay=True, dir_okay=False)) +def offline(file: str): + """ Parse RemoteXPC traffic from a .pcap file """ + sniffer = RemoteXPCSniffer() + for p in sniff(offline=file): + sniffer.process_packet(p) + + +@cli.command() +@click.argument('iface') +def live(iface: str): + """ Parse RemoteXPC live from a given network interface """ + sniffer = RemoteXPCSniffer() + for p in sniff(iface=iface): + sniffer.process_packet(p) + + +if __name__ == '__main__': + cli() From 109da696036ce06d33a95c4756efef4fa2eda04b Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 16 Jul 2023 17:26:47 +0300 Subject: [PATCH 112/234] tss: always add `UID_MODE` to support 16.5.1 updates --- pymobiledevice3/restore/tss.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pymobiledevice3/restore/tss.py b/pymobiledevice3/restore/tss.py index 105493bd8..4868fb611 100644 --- a/pymobiledevice3/restore/tss.py +++ b/pymobiledevice3/restore/tss.py @@ -351,14 +351,9 @@ def add_ap_img4_tags(self, parameters): k = 'SepNonce' self._request[k] = v - uid_mode = parameters.get('UID_MODE') - requires_uid_mode = parameters.get('RequiresUIDMode') - if uid_mode is not None: - self._request['UID_MODE'] = uid_mode - elif requires_uid_mode is not None: - # The logic here is missing why this value is expected to be 'false' - self._request['UID_MODE'] = False + uid_mode = parameters.get('UID_MODE', False) + self._request['UID_MODE'] = uid_mode self._request['@ApImg4Ticket'] = True self._request['@BBTicket'] = True From bdcfb098fd4a0ac36e365a4f82d5014938c7e65f Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 16 Jul 2023 18:34:00 +0300 Subject: [PATCH 113/234] pyproject: bump to version 2.0.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 490738e81..ba1298409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.0.3" +version = "2.0.4" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From d9daf1b774899da7a5f2516ff73f0af296f9cff9 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 17 Jul 2023 08:47:26 +0300 Subject: [PATCH 114/234] remotexpc: fix message-id tracking --- pymobiledevice3/remote/remotexpc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index ef4ce657d..4f314fcb1 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -20,7 +20,7 @@ class RemoteXPCConnection: def __init__(self, address: Tuple[str, int]): self.address = address self.sock: Optional[socket.socket] = None - self.next_message_id = 0 + self.next_message_id: Mapping[int: int] = {1: 0, 3: 0} self.peer_info = None def __enter__(self) -> 'RemoteXPCConnection': @@ -39,7 +39,8 @@ def close(self) -> None: def send_request(self, data: Mapping) -> None: self.sock.sendall( - DataFrame(stream_id=1, data=create_xpc_wrapper(data, message_id=self.next_message_id)).serialize()) + DataFrame(stream_id=1, data=create_xpc_wrapper(data, message_id=self.next_message_id[1])).serialize()) + self.next_message_id[1] += 1 def receive_response(self): while True: @@ -54,7 +55,7 @@ def receive_response(self): if xpc_message.payload.obj.data.entries is None: continue - self.next_message_id = xpc_message.message_id + 1 + self.next_message_id[frame.stream_id] = xpc_message.message_id + 1 return get_object_from_xpc_wrapper(frame.data) def send_receive_request(self, data: Mapping): @@ -75,11 +76,11 @@ def _do_handshake(self) -> None: # send first actual requests self.send_request({}) self._send_frame(DataFrame(stream_id=1, data=XpcWrapper.build({'size': 0, 'flags': 0x0201, 'payload': None}))) - self.next_message_id += 1 + self.next_message_id[1] += 1 self._send_frame(HeadersFrame(stream_id=3, flags=['END_HEADERS'])) self._send_frame( DataFrame(stream_id=3, data=XpcWrapper.build({'size': 0, 'flags': 0x00400001, 'payload': None}))) - self.next_message_id += 1 + self.next_message_id[3] += 1 assert isinstance(self._receive_frame(), SettingsFrame) From 55a544be20bdbadbb4ba4b133e3ddbef04eb390e Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 17 Jul 2023 09:50:14 +0300 Subject: [PATCH 115/234] remotexpc: prevent `next_message_id` incremenet on `send_request()` --- pymobiledevice3/remote/remotexpc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index 4f314fcb1..0224745be 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -40,7 +40,6 @@ def close(self) -> None: def send_request(self, data: Mapping) -> None: self.sock.sendall( DataFrame(stream_id=1, data=create_xpc_wrapper(data, message_id=self.next_message_id[1])).serialize()) - self.next_message_id[1] += 1 def receive_response(self): while True: From f70ece35ad4db2989a509a4ec75c6974884e3b85 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 18 Jul 2023 08:55:15 +0300 Subject: [PATCH 116/234] xpc_message: fix `XpcFlags` --- pymobiledevice3/remote/xpc_message.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index 9fa9590a4..5e5da2000 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -36,9 +36,10 @@ ) XpcFlags = FlagsEnum(Hex(Int32ul), ALWAYS_SET=0x00000001, + PING=0x00000002, DATA_PRESENT=0x00000100, - HEARTBEAT_REQUEST=0x00010000, - HEARTBEAT_RESPONSE=0x00020000, + WANTING_REPLY=0x00010000, + REPLY=0x00020000, FILE_TX_STREAM_REQUEST=0x00100000, FILE_TX_STREAM_RESPONSE=0x00200000, INIT_HANDSHAKE=0x00400000, @@ -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, wanting_reply: bool = False) -> bytes: flags = XpcFlags.ALWAYS_SET if len(d.keys()) > 0: flags |= XpcFlags.DATA_PRESENT + if wanting_reply: + flags |= XpcFlags.WANTING_REPLY xpc_payload = { 'message_id': message_id, From 2a629012cff3cb6679816892a888bf3c36fa6a4d Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 18 Jul 2023 08:55:43 +0300 Subject: [PATCH 117/234] xpc_message: fix array serialization --- pymobiledevice3/remote/xpc_message.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index 5e5da2000..2908eaa68 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -183,7 +183,10 @@ def _build_xpc_array(payload: List) -> Mapping: entries.append(entry) return { 'type': XpcMessageType.ARRAY, - 'data': entries + 'data': { + 'count': len(entries), + 'entries': entries + } } From 77bd7aa6b1a54f5aa4e9519ff17c6bc6e37a1b27 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 18 Jul 2023 08:56:54 +0300 Subject: [PATCH 118/234] remotexpc: fix stream handling --- pymobiledevice3/remote/remotexpc.py | 49 +++++++++++++++++---------- pymobiledevice3/remote/sniffer.py | 7 ++-- pymobiledevice3/remote/xpc_message.py | 13 ++----- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index 0224745be..7c6ecd036 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -2,10 +2,12 @@ from socket import create_connection from typing import Mapping, Optional, Tuple -from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame, SettingsFrame, WindowUpdateFrame +from construct import StreamError +from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame, RstStreamFrame, SettingsFrame, \ + WindowUpdateFrame from pymobiledevice3.exceptions import StreamClosedError -from pymobiledevice3.remote.xpc_message import XpcWrapper, create_xpc_wrapper, get_object_from_xpc_wrapper +from pymobiledevice3.remote.xpc_message import XpcWrapper, create_xpc_wrapper, decode_xpc_object # Extracted by sniffing `remoted` traffic via Wireshark DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS = 100 @@ -15,12 +17,16 @@ FRAME_HEADER_SIZE = 9 HTTP2_MAGIC = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' +ROOT_CHANNEL = 1 +REPLY_CHANNEL = 3 + class RemoteXPCConnection: def __init__(self, address: Tuple[str, int]): + self._previous_frame_data = b'' self.address = address self.sock: Optional[socket.socket] = None - self.next_message_id: Mapping[int: int] = {1: 0, 3: 0} + self.next_message_id: Mapping[int: int] = {ROOT_CHANNEL: 0, REPLY_CHANNEL: 0} self.peer_info = None def __enter__(self) -> 'RemoteXPCConnection': @@ -37,28 +43,35 @@ def connect(self) -> None: def close(self) -> None: self.sock.close() - def send_request(self, data: Mapping) -> None: - self.sock.sendall( - DataFrame(stream_id=1, data=create_xpc_wrapper(data, message_id=self.next_message_id[1])).serialize()) + def send_request(self, data: Mapping, wanting_reply: bool = False) -> None: + xpc_wrapper = create_xpc_wrapper( + data, message_id=self.next_message_id[ROOT_CHANNEL], wanting_reply=wanting_reply) + self.sock.sendall(DataFrame(stream_id=ROOT_CHANNEL, data=xpc_wrapper).serialize()) def receive_response(self): while True: frame = self._receive_frame() if isinstance(frame, GoAwayFrame): - raise StreamClosedError() + raise StreamClosedError(f'Got {frame}') + if isinstance(frame, RstStreamFrame): + raise StreamClosedError(f'Got {frame}') if not isinstance(frame, DataFrame): continue - xpc_message = XpcWrapper.parse(frame.data).message + try: + xpc_message = XpcWrapper.parse(self._previous_frame_data + frame.data).message + self._previous_frame_data = b'' + except StreamError: + self._previous_frame_data += frame.data + continue if xpc_message.payload is None: continue if xpc_message.payload.obj.data.entries is None: continue - self.next_message_id[frame.stream_id] = xpc_message.message_id + 1 - return get_object_from_xpc_wrapper(frame.data) + return decode_xpc_object(xpc_message.payload.obj) def send_receive_request(self, data: Mapping): - self.send_request(data) + self.send_request(data, wanting_reply=True) return self.receive_response() def _do_handshake(self) -> None: @@ -70,16 +83,18 @@ def _do_handshake(self) -> None: SettingsFrame.INITIAL_WINDOW_SIZE: DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE, })) self._send_frame(WindowUpdateFrame(stream_id=0, window_increment=DEFAULT_WIN_SIZE_INCR)) - self._send_frame(HeadersFrame(stream_id=1, flags=['END_HEADERS'])) + self._send_frame(HeadersFrame(stream_id=ROOT_CHANNEL, flags=['END_HEADERS'])) # send first actual requests self.send_request({}) - self._send_frame(DataFrame(stream_id=1, data=XpcWrapper.build({'size': 0, 'flags': 0x0201, 'payload': None}))) - self.next_message_id[1] += 1 - self._send_frame(HeadersFrame(stream_id=3, flags=['END_HEADERS'])) + self._send_frame(DataFrame(stream_id=ROOT_CHANNEL, + data=XpcWrapper.build({'size': 0, 'flags': 0x0201, 'payload': None}))) + self.next_message_id[ROOT_CHANNEL] += 1 + self._send_frame(HeadersFrame(stream_id=REPLY_CHANNEL, flags=['END_HEADERS'])) self._send_frame( - DataFrame(stream_id=3, data=XpcWrapper.build({'size': 0, 'flags': 0x00400001, 'payload': None}))) - self.next_message_id[3] += 1 + DataFrame(stream_id=REPLY_CHANNEL, + data=XpcWrapper.build({'size': 0, 'flags': 0x00400001, 'payload': None}))) + self.next_message_id[REPLY_CHANNEL] += 1 assert isinstance(self._receive_frame(), SettingsFrame) diff --git a/pymobiledevice3/remote/sniffer.py b/pymobiledevice3/remote/sniffer.py index 8e506cd7f..483791e63 100644 --- a/pymobiledevice3/remote/sniffer.py +++ b/pymobiledevice3/remote/sniffer.py @@ -14,7 +14,7 @@ from scapy.sendrecv import sniff from pymobiledevice3.remote.core_device_tunnel_service import PairingDataComponentTLVBuf -from pymobiledevice3.remote.xpc_message import get_object_from_xpc_wrapper +from pymobiledevice3.remote.xpc_message import XpcWrapper, decode_xpc_object logger = logging.getLogger() @@ -130,7 +130,10 @@ def _process_tcp(self, pkt: Packet) -> None: def _handle_data_frame(self, stream: H2Stream, frame: DataFrame) -> None: previous_frame_data = self._previous_frame_data.get(stream.key, b'') try: - xpc_message = get_object_from_xpc_wrapper(previous_frame_data + frame.data) + payload = XpcWrapper.parse(previous_frame_data + frame.data).message.payload + if payload is None: + return None + xpc_message = decode_xpc_object(payload.obj) except ConstError: # if we don't know what this payload is logger.debug( f'New Data frame {stream.src}->{stream.dst} on HTTP/2 stream {frame.stream_id} TCP port {stream.dport}') diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index 2908eaa68..c33e1166c 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -117,14 +117,14 @@ def _decode_xpc_dictionary(xpc_object) -> Mapping: return {} result = {} for entry in xpc_object.data.entries: - result[entry.key] = _decode_xpc_object(entry.value) + result[entry.key] = decode_xpc_object(entry.value) return result def _decode_xpc_array(xpc_object) -> List: result = [] for entry in xpc_object.data.entries: - result.append(_decode_xpc_object(entry)) + result.append(decode_xpc_object(entry)) return result @@ -152,7 +152,7 @@ def _decode_xpc_data(xpc_object) -> bytes: return xpc_object.data -def _decode_xpc_object(xpc_object) -> Any: +def decode_xpc_object(xpc_object) -> Any: decoders = { XpcMessageType.DICTIONARY: _decode_xpc_dictionary, XpcMessageType.ARRAY: _decode_xpc_array, @@ -169,13 +169,6 @@ def _decode_xpc_object(xpc_object) -> Any: return decoder(xpc_object) -def get_object_from_xpc_wrapper(payload: bytes): - payload = XpcWrapper.parse(payload).message.payload - if payload is None: - return None - return _decode_xpc_object(payload.obj) - - def _build_xpc_array(payload: List) -> Mapping: entries = [] for entry in payload: From 974d653cee12f5f6a9c4c7a87a6c9aada594a859 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 18 Jul 2023 13:12:28 +0300 Subject: [PATCH 119/234] add support for remote lockdown using `LockdownServiceProvider` --- pymobiledevice3/cli/afc.py | 2 +- pymobiledevice3/cli/cli_common.py | 18 +++++- pymobiledevice3/cli/lockdown.py | 2 +- pymobiledevice3/cli/remote.py | 61 ++++-------------- pymobiledevice3/exceptions.py | 6 +- pymobiledevice3/lockdown.py | 48 +++++++------- pymobiledevice3/lockdown_service_provider.py | 30 +++++++++ .../remote/core_device_tunnel_service.py | 2 +- .../remote/remote_service_discovery.py | 62 +++++++++++++++++-- pymobiledevice3/restore/asr.py | 4 +- pymobiledevice3/restore/fdr.py | 6 +- pymobiledevice3/restore/restore.py | 10 +-- pymobiledevice3/restore/restored_client.py | 4 +- pymobiledevice3/service_connection.py | 19 +++--- .../services/accessibilityaudit.py | 9 ++- pymobiledevice3/services/afc.py | 22 +++++-- pymobiledevice3/services/amfi.py | 6 +- pymobiledevice3/services/companion.py | 23 ++++--- pymobiledevice3/services/crash_reports.py | 23 +++++-- .../services/debugserver_applist.py | 4 +- .../services/device_arbitration.py | 4 +- pymobiledevice3/services/diagnostics.py | 30 +++++---- pymobiledevice3/services/dtfetchsymbols.py | 2 +- .../services/dvt/dvt_secure_socket_proxy.py | 11 +++- .../instruments/core_profile_session_tap.py | 11 +++- .../services/dvt/instruments/device_info.py | 6 +- pymobiledevice3/services/file_relay.py | 4 +- pymobiledevice3/services/heartbeat.py | 15 +++-- pymobiledevice3/services/house_arrest.py | 14 ++--- .../services/installation_proxy.py | 10 ++- .../{base_service.py => lockdown_service.py} | 19 +++--- pymobiledevice3/services/misagent.py | 16 +++-- pymobiledevice3/services/mobile_activation.py | 4 +- pymobiledevice3/services/mobile_config.py | 10 ++- .../services/mobile_image_mounter.py | 12 ++-- pymobiledevice3/services/mobilebackup2.py | 4 +- .../services/notification_proxy.py | 29 ++++++--- pymobiledevice3/services/os_trace.py | 15 +++-- pymobiledevice3/services/pcapd.py | 13 ++-- pymobiledevice3/services/power_assertion.py | 13 ++-- pymobiledevice3/services/preboard.py | 13 ++-- pymobiledevice3/services/remote_server.py | 11 ++-- pymobiledevice3/services/screenshot.py | 4 +- pymobiledevice3/services/simulate_location.py | 8 +-- pymobiledevice3/services/springboard.py | 13 ++-- pymobiledevice3/services/syslog.py | 13 ++-- pymobiledevice3/services/webinspector.py | 16 +++-- pymobiledevice3/tcp_forwarder.py | 4 +- 48 files changed, 434 insertions(+), 251 deletions(-) create mode 100644 pymobiledevice3/lockdown_service_provider.py rename pymobiledevice3/services/{base_service.py => lockdown_service.py} (53%) diff --git a/pymobiledevice3/cli/afc.py b/pymobiledevice3/cli/afc.py index b832f7056..9f37bd65d 100644 --- a/pymobiledevice3/cli/afc.py +++ b/pymobiledevice3/cli/afc.py @@ -20,7 +20,7 @@ def afc(): @afc.command('shell', cls=Command) def afc_shell(lockdown: LockdownClient): """ open an AFC shell rooted at /var/mobile/Media """ - AfcShell(lockdown=lockdown, service_name='com.apple.afc').cmdloop() + AfcShell(lockdown=lockdown).cmdloop() @afc.command('pull', cls=Command) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index 7418d1870..f8e275f39 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -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 @@ -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 @@ -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.lockdown_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.lockdown_service_provider = rsd + return self.lockdown_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.lockdown_service_provider is not None: + return self.lockdown_service_provider + if value is not None: return create_using_usbmux(serial=value) diff --git a/pymobiledevice3/cli/lockdown.py b/pymobiledevice3/cli/lockdown.py index 398e9dea2..e4e054dfa 100644 --- a/pymobiledevice3/cli/lockdown.py +++ b/pymobiledevice3/cli/lockdown.py @@ -32,7 +32,7 @@ def lockdown_recovery(lockdown: LockdownClient): @click.argument('service_name') def lockdown_service(lockdown: LockdownClient, service_name): """ send-receive raw service messages """ - lockdown.start_service(service_name).shell() + lockdown.start_lockdown_service(service_name).shell() @lockdown_group.command('info', cls=Command) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index fbc31d5ef..da2e52956 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -1,48 +1,16 @@ import asyncio import logging -import os -from typing import Optional 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.cli.cli_common import Command, print_json 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.remote_service_discovery import RemoteServiceDiscoveryService logger = logging.getLogger(__name__) -class RemoteCommand(click.Command): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.params[:0] = [ - click.Option(('hostname', '--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), - ] - - @staticmethod - def udid(ctx, param: str, value: str) -> Optional[str]: - if '_PYMOBILEDEVICE3_COMPLETE' in os.environ: - # prevent lockdown connection establishment when in autocomplete mode - return - - if value is not None: - return get_remoted_device(udid=value).hostname - - device_options = get_remoted_devices() - if len(device_options) == 0: - raise NoDeviceConnectedError() - elif len(device_options) == 1: - return device_options[0].hostname - - return prompt_device_list(device_options).hostname - - @click.group() def cli(): """ remote cli """ @@ -55,31 +23,28 @@ def remote_cli(): pass -@remote_cli.command('rsd-info', cls=RemoteCommand) +@remote_cli.command('rsd-info', cls=Command) @click.option('--color/--no-color', default=True) -def rsd_info(hostname: str, color: bool): +def rsd_info(lockdown: RemoteServiceDiscoveryService, color: bool): """ show info extracted from RSD peer """ - with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd: - print_json(rsd.peer_info, colored=color) + print_json(lockdown.peer_info, colored=color) -@remote_cli.command('create-listener', cls=RemoteCommand) +@remote_cli.command('create-listener', cls=Command) @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): +def create_listener(lockdown: RemoteServiceDiscoveryService, protocol: str, color: bool): """ start a remote listener """ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - with RemoteServiceDiscoveryService((hostname, RSD_PORT)) as rsd: - with create_core_device_tunnel_service(rsd, autopair=True) as service: - print_json(service.create_listener(private_key, protocol=protocol), colored=color) + with create_core_device_tunnel_service(lockdown, autopair=True) as service: + 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=Command) @click.option('--color/--no-color', default=True) -def start_quic_tunnel(hostname: str, color: bool): +def start_quic_tunnel(lockdown: RemoteServiceDiscoveryService, color: bool): """ start quic tunnel """ logger.critical('This is a WIP command. Will only print the required parameters for the quic connection') private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - 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) + with create_core_device_tunnel_service(lockdown, autopair=True) as service: + print_json(asyncio.run(service.start_quic_tunnel(private_key)), colored=color) diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index 83f063a24..6e6ca02e7 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -10,7 +10,7 @@ 'MissingValueError', 'PasscodeRequiredError', 'AmfiError', 'DeviceHasPasscodeSetError', 'NotificationTimeoutError', 'DeveloperModeError', 'ProfileError', 'IRecvError', 'IRecvNoDeviceConnectedError', 'NoDeviceSelectedError', 'MessageNotSupportedError', 'InvalidServiceError', 'InspectorEvaluateError', - 'LaunchingApplicationError', 'BadCommandError', 'BadDevError', 'ConnectionFailedError', + 'LaunchingApplicationError', 'BadCommandError', 'BadDevError', 'ConnectionFailedError', 'CoreDeviceError' ] @@ -284,3 +284,7 @@ class LaunchingApplicationError(PyMobileDevice3Exception): class AppInstallError(PyMobileDevice3Exception): pass + + +class CoreDeviceError(PyMobileDevice3Exception): + pass diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 9380fb1c4..35e02bbf4 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -21,9 +21,10 @@ MissingValueError, NotPairedError, PairingDialogResponsePendingError, PairingError, PasswordRequiredError, \ SetProhibitedError, StartServiceError, UserDeniedPairingError from pymobiledevice3.irecv_devices import IRECV_DEVICES +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider 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.service_connection import LockdownServiceConnection from pymobiledevice3.usbmux import PlistMuxConnection from pymobiledevice3.utils import sanitize_ios_version @@ -100,8 +101,9 @@ def _inner_reconnect_on_remote_close(*args, **kwargs): return _inner_reconnect_on_remote_close -class LockdownClient(ABC): - def __init__(self, service: ServiceConnection, host_id: str, identifier: str = None, label: str = DEFAULT_LABEL, +class LockdownClient(ABC, LockdownServiceProvider): + def __init__(self, service: LockdownServiceConnection, 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): """ @@ -135,11 +137,10 @@ def __init__(self, service: ServiceConnection, host_id: str, identifier: str = N self.udid = self.all_values.get('UniqueDeviceID') self.unique_chip_id = self.all_values.get('UniqueChipID') self.device_public_key = self.all_values.get('DevicePublicKey') - self.product_version = self.all_values.get('ProductVersion') self.product_type = self.all_values.get('ProductType') @classmethod - def create(cls, service: ServiceConnection, identifier: str = None, system_buid: str = SYSTEM_BUID, + def create(cls, service: LockdownServiceConnection, identifier: str = None, system_buid: str = SYSTEM_BUID, label: str = DEFAULT_LABEL, autopair: bool = True, pair_timeout: int = None, local_hostname: str = None, pair_record: Mapping = None, pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT, **cls_specific_args): @@ -180,6 +181,10 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close() + @property + def product_version(self) -> str: + return self.all_values.get('ProductVersion') + @property def device_class(self) -> DeviceClass: try: @@ -446,7 +451,7 @@ def get_service_connection_attributes(self, name, escrow_bag=None) -> Mapping: return response @_reconnect_on_remote_close - def start_service(self, name: str, escrow_bag=None) -> ServiceConnection: + def start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: attr = self.get_service_connection_attributes(name, escrow_bag=escrow_bag) service_connection = self._create_service_connection(attr['Port']) @@ -455,7 +460,7 @@ def start_service(self, name: str, escrow_bag=None) -> ServiceConnection: service_connection.ssl_start(f) return service_connection - async def aio_start_service(self, name: str, escrow_bag=None) -> ServiceConnection: + async def aio_start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: attr = self.get_service_connection_attributes(name, escrow_bag=escrow_bag) service_connection = self._create_service_connection(attr['Port']) @@ -464,16 +469,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() @@ -507,7 +502,7 @@ def _handle_autopair(self, autopair: bool, timeout: int) -> None: raise FatalPairingError() @abstractmethod - def _create_service_connection(self, port: int) -> ServiceConnection: + def _create_service_connection(self, port: int) -> LockdownServiceConnection: """ Used to establish a new ServiceConnection to a given port """ pass @@ -569,8 +564,9 @@ def short_info(self) -> Dict: short_info['ConnectionType'] = self.service.mux_device.connection_type return short_info - def _create_service_connection(self, port: int) -> ServiceConnection: - return ServiceConnection.create_using_usbmux(self.identifier, port, self.service.mux_device.connection_type) + def _create_service_connection(self, port: int) -> LockdownServiceConnection: + return LockdownServiceConnection.create_using_usbmux(self.identifier, port, + self.service.mux_device.connection_type) class PlistUsbmuxLockdownClient(UsbmuxLockdownClient): @@ -582,7 +578,7 @@ def save_pair_record(self) -> None: class TcpLockdownClient(LockdownClient): - def __init__(self, service: ServiceConnection, host_id: str, hostname: str, identifier: str = None, + def __init__(self, service: LockdownServiceConnection, host_id: str, hostname: 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): """ @@ -602,8 +598,8 @@ def __init__(self, service: ServiceConnection, host_id: str, hostname: str, iden port) self.hostname = hostname - def _create_service_connection(self, port: int) -> ServiceConnection: - return ServiceConnection.create_using_tcp(self.hostname, port) + def _create_service_connection(self, port: int) -> LockdownServiceConnection: + return LockdownServiceConnection.create_using_tcp(self.hostname, port) def create_using_usbmux(serial: str = None, identifier: str = None, label: str = DEFAULT_LABEL, autopair: bool = True, @@ -625,7 +621,7 @@ def create_using_usbmux(serial: str = None, identifier: str = None, label: str = :param port: lockdownd service port :return: UsbmuxLockdownClient instance """ - service = ServiceConnection.create_using_usbmux(serial, port, connection_type=connection_type) + service = LockdownServiceConnection.create_using_usbmux(serial, port, connection_type=connection_type) cls = UsbmuxLockdownClient with usbmux.create_mux() as client: if isinstance(client, PlistMuxConnection): @@ -660,7 +656,7 @@ def create_using_tcp(hostname: str, identifier: str = None, label: str = DEFAULT :param port: lockdownd service port :return: TcpLockdownClient instance """ - service = ServiceConnection.create_using_tcp(hostname, port) + service = LockdownServiceConnection.create_using_tcp(hostname, port) client = TcpLockdownClient.create( service, identifier=identifier, label=label, local_hostname=local_hostname, pair_record=pair_record, pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, autopair=autopair, @@ -686,7 +682,7 @@ def create_using_remote(hostname: str, identifier: str = None, label: str = DEFA :param port: lockdownd service port :return: TcpLockdownClient instance """ - service = ServiceConnection.create_using_tcp(hostname, port) + service = LockdownServiceConnection.create_using_tcp(hostname, port) service.send_plist({'Label': label, 'ProtocolVersion': '2', 'Request': 'RSDCheckin'}) # we expect two responses after the first request diff --git a/pymobiledevice3/lockdown_service_provider.py b/pymobiledevice3/lockdown_service_provider.py new file mode 100644 index 000000000..205307308 --- /dev/null +++ b/pymobiledevice3/lockdown_service_provider.py @@ -0,0 +1,30 @@ +import logging +from abc import abstractmethod + +from pymobiledevice3.exceptions import StartServiceError +from pymobiledevice3.service_connection import LockdownServiceConnection + + +class LockdownServiceProvider: + @property + @abstractmethod + def product_version(self) -> str: + pass + + @abstractmethod + def start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: + pass + + @abstractmethod + async def aio_start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: + pass + + def start_lockdown_developer_service(self, name, escrow_bag: bytes = None) -> LockdownServiceConnection: + try: + return self.start_lockdown_service(name, escrow_bag=escrow_bag) + 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 diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index f4985d87d..b5bba282b 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -126,7 +126,7 @@ def __init__(self, rsd: RemoteServiceDiscoveryService): self.signature = None def connect(self, autopair: bool = True) -> None: - self.service = self.rsd.connect_to_service('com.apple.internal.dt.coredevice.untrusted.tunnelservice') + self.service = self.rsd.start_remote_service('com.apple.internal.dt.coredevice.untrusted.tunnelservice') self.version = self.service.receive_response()['ServiceVersion'] self._attempt_pair_verify() diff --git a/pymobiledevice3/remote/remote_service_discovery.py b/pymobiledevice3/remote/remote_service_discovery.py index 5c3342c14..654ef3c53 100644 --- a/pymobiledevice3/remote/remote_service_discovery.py +++ b/pymobiledevice3/remote/remote_service_discovery.py @@ -1,9 +1,13 @@ +import logging from dataclasses import dataclass -from typing import List, Tuple +from typing import List, Tuple, Union -from pymobiledevice3.exceptions import NoDeviceConnectedError +from pymobiledevice3.exceptions import InvalidServiceError, NoDeviceConnectedError, PyMobileDevice3Exception, \ + StartServiceError +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.remote.bonjour import DEFAULT_BONJOUR_TIMEOUT, get_remoted_addresses from pymobiledevice3.remote.remotexpc import RemoteXPCConnection +from pymobiledevice3.service_connection import LockdownServiceConnection @dataclass @@ -18,21 +22,61 @@ 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 + @property + def product_version(self) -> str: + return self.peer_info['Properties']['OSVersion'] + 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_lockdown_service_without_checkin(self, name: str) -> LockdownServiceConnection: + return LockdownServiceConnection.create_using_tcp(self.service.address[0], self._get_service_port(name)) + + def start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: + service = self.start_lockdown_service_without_checkin(name) + 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 + + async def aio_start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: + service = self.start_lockdown_service(name, escrow_bag=escrow_bag) + await service.aio_start() + return service + + def start_lockdown_developer_service(self, name, escrow_bag: bytes = None) -> LockdownServiceConnection: + try: + return self.start_lockdown_service_without_checkin(name) + 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 + + def start_remote_service(self, name: str) -> RemoteXPCConnection: + service = RemoteXPCConnection((self.service.address[0], self._get_service_port(name))) service.connect() return service + def start_service(self, name: str) -> Union[RemoteXPCConnection, LockdownServiceConnection]: + service = self.peer_info['Services'][name] + service_properties = service.get('Properties', {}) + use_remote_xpc = service_properties.get('UsesRemoteXPC', False) + return self.start_remote_service(name) if use_remote_xpc else self.start_lockdown_service(name) + def __enter__(self) -> 'RemoteServiceDiscoveryService': self.connect() return self @@ -40,6 +84,12 @@ def __enter__(self) -> 'RemoteServiceDiscoveryService': def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.service.close() + def _get_service_port(self, name: str) -> int: + service = self.peer_info['Services'].get(name) + if service is None: + raise InvalidServiceError(f'No such service: {name}') + return int(service['Port']) + def get_remoted_devices(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[RSDDevice]: result = [] diff --git a/pymobiledevice3/restore/asr.py b/pymobiledevice3/restore/asr.py index c9d14a81e..984106671 100644 --- a/pymobiledevice3/restore/asr.py +++ b/pymobiledevice3/restore/asr.py @@ -7,7 +7,7 @@ from tqdm import trange from pymobiledevice3.exceptions import PyMobileDevice3Exception -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.service_connection import LockdownServiceConnection ASR_VERSION = 1 ASR_STREAM_ID = 1 @@ -29,7 +29,7 @@ class ASRClient(object): SERVICE_PORT = ASR_PORT def __init__(self, udid: str): - self.service = ServiceConnection.create_using_usbmux(udid, self.SERVICE_PORT) + self.service = LockdownServiceConnection.create_using_usbmux(udid, self.SERVICE_PORT) # receive Initiate command message data = self.recv_plist() diff --git a/pymobiledevice3/restore/fdr.py b/pymobiledevice3/restore/fdr.py index 5c3812d48..2b0a20e37 100644 --- a/pymobiledevice3/restore/fdr.py +++ b/pymobiledevice3/restore/fdr.py @@ -8,7 +8,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError, NoDeviceConnectedError, PyMobileDevice3Exception -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.service_connection import LockdownServiceConnection CTRL_PORT = 0x43a # 1082 CTRLCMD = b'BeginCtrl\0' @@ -48,10 +48,10 @@ def __init__(self, type_: fdr_type, udid=None): logger.debug('connecting to FDR') if type_ == fdr_type.FDR_CTRL: - self.service = ServiceConnection.create_using_usbmux(device.serial, self.SERVICE_PORT) + self.service = LockdownServiceConnection.create_using_usbmux(device.serial, self.SERVICE_PORT) self.ctrl_handshake() else: - self.service = ServiceConnection.create_using_usbmux(device.serial, conn_port) + self.service = LockdownServiceConnection.create_using_usbmux(device.serial, conn_port) self.sync_handshake() logger.debug('FDR connected') diff --git a/pymobiledevice3/restore/restore.py b/pymobiledevice3/restore/restore.py index 4b502f577..44925a758 100644 --- a/pymobiledevice3/restore/restore.py +++ b/pymobiledevice3/restore/restore.py @@ -23,7 +23,7 @@ from pymobiledevice3.restore.restore_options import RestoreOptions from pymobiledevice3.restore.restored_client import RestoredClient from pymobiledevice3.restore.tss import TSSRequest, TSSResponse -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.service_connection import LockdownServiceConnection from pymobiledevice3.utils import plist_access_path known_errors = { @@ -48,7 +48,7 @@ def __init__(self, ipsw: zipfile.ZipFile, device: Device, tss=None, behavior: Be # used when ignore_fdr=True, to store an active FDR connection just to make the device believe it can actually # perform an FDR communication, but without really establishing any - self._fdr: Optional[ServiceConnection] = None + self._fdr: Optional[LockdownServiceConnection] = None self._ignore_fdr = ignore_fdr # query preflight info while device may still be in normal mode @@ -633,7 +633,7 @@ def send_bootability_bundle_data(self, message): while True: try: - client = ServiceConnection.create_using_usbmux(self._restored.udid, data_port) + client = LockdownServiceConnection.create_using_usbmux(self._restored.udid, data_port) break except ConnectionFailedError: self.logger.debug('Retrying connection...') @@ -1153,7 +1153,7 @@ def handle_baseband_updater_output_data(self, message: Mapping): while True: try: - client = ServiceConnection.create_using_usbmux(self._restored.udid, data_port) + client = LockdownServiceConnection.create_using_usbmux(self._restored.udid, data_port) break except ConnectionFailedError: self.logger.debug('Retrying connection...') @@ -1198,7 +1198,7 @@ def restore_device(self): if self._ignore_fdr: self.logger.info('Establishing a mock FDR listener') - self._fdr = ServiceConnection.create_using_usbmux(self._restored.udid, FDRClient.SERVICE_PORT) + self._fdr = LockdownServiceConnection.create_using_usbmux(self._restored.udid, FDRClient.SERVICE_PORT) else: self.logger.info('Starting FDR listener thread') start_fdr_thread(fdr_type.FDR_CTRL) diff --git a/pymobiledevice3/restore/restored_client.py b/pymobiledevice3/restore/restored_client.py index 5bfca990c..fcd4491d5 100644 --- a/pymobiledevice3/restore/restored_client.py +++ b/pymobiledevice3/restore/restored_client.py @@ -4,7 +4,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError, NoDeviceConnectedError from pymobiledevice3.restore.restore_options import RestoreOptions -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.service_connection import LockdownServiceConnection class RestoredClient(object): @@ -14,7 +14,7 @@ class RestoredClient(object): def __init__(self, udid=None, client_name=DEFAULT_CLIENT_NAME): self.logger = logging.getLogger(__name__) self.udid = self._get_or_verify_udid(udid) - self.service = ServiceConnection.create_using_usbmux(self.udid, self.SERVICE_PORT) + self.service = LockdownServiceConnection.create_using_usbmux(self.udid, self.SERVICE_PORT) self.label = client_name self.query_type = self.service.send_recv_plist({'Request': 'QueryType'}) self.version = self.query_type.get('RestoreProtocolVersion') diff --git a/pymobiledevice3/service_connection.py b/pymobiledevice3/service_connection.py index 3d9afc76f..247a8fc61 100755 --- a/pymobiledevice3/service_connection.py +++ b/pymobiledevice3/service_connection.py @@ -62,7 +62,7 @@ class Medium(Enum): USBMUX = auto() -class ServiceConnection: +class LockdownServiceConnection: """ wrapper for usbmux tcp-relay connections """ def __init__(self, sock: socket.socket, mux_device: MuxDevice = None): @@ -76,26 +76,26 @@ def __init__(self, sock: socket.socket, mux_device: MuxDevice = None): self._writer = None # type: Optional[asyncio.StreamWriter] @staticmethod - def create_using_tcp(hostname: str, port: int) -> 'ServiceConnection': + def create_using_tcp(hostname: str, port: int) -> 'LockdownServiceConnection': sock = socket.create_connection((hostname, port)) - return ServiceConnection(sock) + return LockdownServiceConnection(sock) @staticmethod - def create_using_usbmux(udid: Optional[str], port: int, connection_type: str = None) -> 'ServiceConnection': + def create_using_usbmux(udid: Optional[str], port: int, connection_type: str = None) -> 'LockdownServiceConnection': target_device = select_device(udid, connection_type=connection_type) if target_device is None: if udid: raise ConnectionFailedError() raise NoDeviceConnectedError() sock = target_device.connect(port) - return ServiceConnection(sock, mux_device=target_device) + return LockdownServiceConnection(sock, mux_device=target_device) @staticmethod - def create(medium: Medium, identifier: str, port: int, connection_type: str = None) -> 'ServiceConnection': + def create(medium: Medium, identifier: str, port: int, connection_type: str = None) -> 'LockdownServiceConnection': if medium == Medium.TCP: - return ServiceConnection.create_using_tcp(identifier, port) + return LockdownServiceConnection.create_using_tcp(identifier, port) else: - return ServiceConnection.create_using_usbmux(identifier, port, connection_type=connection_type) + return LockdownServiceConnection.create_using_usbmux(identifier, port, connection_type=connection_type) def setblocking(self, blocking: bool) -> None: self.socket.setblocking(blocking) @@ -187,6 +187,9 @@ async def aio_ssl_start(self, certfile, keyfile=None) -> None: server_hostname='' ) + async def aio_start(self) -> None: + self._reader, self._writer = await asyncio.open_connection(sock=self.socket) + def shell(self) -> None: IPython.embed( header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.TerminalTrueColorFormatter(style='native')), diff --git a/pymobiledevice3/services/accessibilityaudit.py b/pymobiledevice3/services/accessibilityaudit.py index 5435f8eb2..7f0907ad0 100644 --- a/pymobiledevice3/services/accessibilityaudit.py +++ b/pymobiledevice3/services/accessibilityaudit.py @@ -5,6 +5,7 @@ from packaging.version import Version from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.services.remote_server import MessageAux, RemoteServer @@ -115,9 +116,13 @@ def deserialize_object(d): class AccessibilityAudit(RemoteServer): SERVICE_NAME = 'com.apple.accessibility.axAuditDaemon.remoteserver' + RSD_SERVICE_NAME = 'com.apple.accessibility.axAuditDaemon.remoteserver.shim.remote' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME, remove_ssl_context=True) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME, remove_ssl_context=True, is_developer_service=False) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME, remove_ssl_context=True, is_developer_service=False) # flush previously received messages self.recv_plist() diff --git a/pymobiledevice3/services/afc.py b/pymobiledevice3/services/afc.py index 0f62f01be..3a82749c6 100755 --- a/pymobiledevice3/services/afc.py +++ b/pymobiledevice3/services/afc.py @@ -23,7 +23,8 @@ from pymobiledevice3.exceptions import AfcException, AfcFileNotFoundError, ArgumentError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService from pymobiledevice3.utils import try_decode MAXIMUM_READ_SIZE = 1 * 1024 ** 2 # 1 MB @@ -203,8 +204,16 @@ def list_to_dict(d): return res -class AfcService(BaseService): - def __init__(self, lockdown: LockdownClient, service_name='com.apple.afc'): +class AfcService(LockdownService): + SERVICE_NAME = 'com.apple.afc' + RSD_SERVICE_NAME = 'com.apple.afc.shim.remote' + + def __init__(self, lockdown: LockdownServiceProvider, service_name: str = None): + if service_name is None: + if isinstance(lockdown, LockdownClient): + service_name = self.SERVICE_NAME + else: + service_name = self.RSD_SERVICE_NAME super().__init__(lockdown, service_name) self.packet_num = 0 @@ -674,16 +683,17 @@ def print(self, *objects, sep=' ', end='\n', file=sys.stdout, flush=False): class AfcShell(Cmd): - def __init__(self, lockdown: LockdownClient, service_name='com.apple.afc', completekey='tab', afc_service=None): + def __init__(self, lockdown: LockdownServiceProvider, service_name: str = None, completekey: str = 'tab', + afc_service: LockdownService = None): # bugfix: prevent the Cmd instance from trying to parse click's arguments sys.argv = sys.argv[:1] Cmd.__init__(self, completekey=completekey, persistent_history_file=os.path.join(tempfile.gettempdir(), f'.{service_name}-history')) + self.logger = logging.getLogger(__name__) self.lockdown = lockdown - self.service_name = service_name self.afc = afc_service or AfcService(self.lockdown, service_name=service_name) self.curdir = '/' self.complete_edit = self._complete_first_arg @@ -811,7 +821,7 @@ def relative_path(self, filename): return posixpath.join(self.curdir, filename) def _update_prompt(self): - self.prompt = highlight(f'[{self.service_name}:{self.curdir}]$ ', lexers.BashSessionLexer(), + self.prompt = highlight(f'[{self.afc.service_name}:{self.curdir}]$ ', lexers.BashSessionLexer(), formatters.TerminalTrueColorFormatter(style='solarized-dark')).strip() def _complete(self, text, line, begidx, endidx): diff --git a/pymobiledevice3/services/amfi.py b/pymobiledevice3/services/amfi.py index 2a6ca70f5..de4bbcf9f 100644 --- a/pymobiledevice3/services/amfi.py +++ b/pymobiledevice3/services/amfi.py @@ -18,7 +18,7 @@ def __init__(self, lockdown: LockdownClient): def create_amfi_show_override_path_file(self): """ create an empty file at AMFIShowOverridePath """ - service = self._lockdown.start_service(self.SERVICE_NAME) + service = self._lockdown.start_lockdown_service(self.SERVICE_NAME) resp = service.send_recv_plist({'action': 0}) if not resp['status']: raise PyMobileDevice3Exception(f'create_AMFIShowOverridePath() failed with: {resp}') @@ -29,7 +29,7 @@ def enable_developer_mode(self, enable_post_restart=True): if enable_post_restart is True, then wait for device restart to answer the final prompt with "yes" """ - service = self._lockdown.start_service(self.SERVICE_NAME) + service = self._lockdown.start_lockdown_service(self.SERVICE_NAME) resp = service.send_recv_plist({'action': 1}) error = resp.get('Error') @@ -60,7 +60,7 @@ def enable_developer_mode(self, enable_post_restart=True): def enable_developer_mode_post_restart(self): """ answer the prompt that appears after the restart with "yes" """ - service = self._lockdown.start_service(self.SERVICE_NAME) + service = self._lockdown.start_lockdown_service(self.SERVICE_NAME) resp = service.send_recv_plist({'action': 2}) if not resp.get('success'): raise DeveloperModeError(f'enable_developer_mode_post_restart() failed: {resp}') diff --git a/pymobiledevice3/services/companion.py b/pymobiledevice3/services/companion.py index 6439c1be5..54d4a08b6 100644 --- a/pymobiledevice3/services/companion.py +++ b/pymobiledevice3/services/companion.py @@ -3,27 +3,32 @@ from pymobiledevice3.exceptions import PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService -class CompanionProxyService(BaseService): +class CompanionProxyService(LockdownService): SERVICE_NAME = 'com.apple.companion_proxy' + RSD_SERVICE_NAME = 'com.apple.companion_proxy.shim.remote' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def list(self): - service = self.lockdown.start_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.service_name) return service.send_recv_plist({'Command': 'GetDeviceRegistry'}).get('PairedDevicesArray', []) def listen_for_devices(self): - service = self.lockdown.start_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.service_name) service.send_plist({'Command': 'StartListeningForDevices'}) while True: yield service.recv_plist() def get_value(self, udid: str, key: str): - service = self.lockdown.start_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.service_name) response = service.send_recv_plist({'Command': 'GetValueFromRegistry', 'GetValueGizmoUDIDKey': udid, 'GetValueKeyKey': key}) @@ -36,7 +41,7 @@ def get_value(self, udid: str, key: str): raise PyMobileDevice3Exception(error) def start_forwarding_service_port(self, remote_port: int, service_name: str = None, options: typing.Mapping = None): - service = self.lockdown.start_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.service_name) request = {'Command': 'StartForwardingServicePort', 'GizmoRemotePortNumber': remote_port, @@ -52,7 +57,7 @@ def start_forwarding_service_port(self, remote_port: int, service_name: str = No return service.send_recv_plist(request).get('CompanionProxyServicePort') def stop_forwarding_service_port(self, remote_port: int): - service = self.lockdown.start_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.service_name) request = {'Command': 'StopForwardingServicePort', 'GizmoRemotePortNumber': remote_port} diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index e6ac45444..6a311e680 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -7,6 +7,7 @@ from pymobiledevice3.exceptions import AfcException from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.services.afc import AfcService, AfcShell from pymobiledevice3.services.os_trace import OsTraceService @@ -15,14 +16,26 @@ class CrashReportsManager: COPY_MOBILE_NAME = 'com.apple.crashreportcopymobile' + RSD_COPY_MOBILE_NAME = 'com.apple.crashreportcopymobile.shim.remote' + CRASH_MOVER_NAME = 'com.apple.crashreportmover' + RSD_CRASH_MOVER_NAME = 'com.apple.crashreportmover.shim.remote' + APPSTORED_PATH = '/com.apple.appstored' IN_PROGRESS_SYSDIAGNOSE_EXTENSIONS = ['.tmp', '.tar.gz'] - def __init__(self, lockdown: LockdownClient): + def __init__(self, lockdown: LockdownServiceProvider): self.logger = logging.getLogger(__name__) self.lockdown = lockdown - self.afc = AfcService(lockdown, service_name=self.COPY_MOBILE_NAME) + + if isinstance(lockdown, LockdownClient): + self.copy_mobile_service_name = self.COPY_MOBILE_NAME + self.crash_mover_service_name = self.CRASH_MOVER_NAME + else: + self.copy_mobile_service_name = self.RSD_COPY_MOBILE_NAME + self.crash_mover_service_name = self.RSD_CRASH_MOVER_NAME + + self.afc = AfcService(lockdown, service_name=self.copy_mobile_service_name) def __enter__(self): return self @@ -78,7 +91,7 @@ def log(src, dst): def flush(self) -> None: """ Trigger com.apple.crashreportmover to flush all products into CrashReports directory """ ack = b'ping\x00' - assert ack == self.lockdown.start_service(self.CRASH_MOVER_NAME).recvall(len(ack)) + assert ack == self.lockdown.start_lockdown_service(self.crash_mover_service_name).recvall(len(ack)) def watch(self, name: str = None, raw: bool = False) -> Generator[str, None, None]: """ @@ -149,9 +162,9 @@ def get_new_sysdiagnose(self, out: str, erase: bool = True) -> None: class CrashReportsShell(AfcShell): - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, service_name=CrashReportsManager.COPY_MOBILE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): self.manager = CrashReportsManager(lockdown) + super().__init__(lockdown, service_name=self.manager.copy_mobile_service_name) self.complete_parse = self._complete_first_arg @with_argparser(parse_parser) diff --git a/pymobiledevice3/services/debugserver_applist.py b/pymobiledevice3/services/debugserver_applist.py index d1e85cd01..0e2f6313f 100755 --- a/pymobiledevice3/services/debugserver_applist.py +++ b/pymobiledevice3/services/debugserver_applist.py @@ -2,12 +2,12 @@ import typing from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService CHUNK_SIZE = 200 -class DebugServerAppList(BaseService): +class DebugServerAppList(LockdownService): SERVICE_NAME = 'com.apple.debugserver.DVTSecureSocketProxy.applist' def __init__(self, lockdown: LockdownClient): diff --git a/pymobiledevice3/services/device_arbitration.py b/pymobiledevice3/services/device_arbitration.py index 913451b16..cf643bdbf 100755 --- a/pymobiledevice3/services/device_arbitration.py +++ b/pymobiledevice3/services/device_arbitration.py @@ -2,10 +2,10 @@ from pymobiledevice3.exceptions import ArbitrationError, DeviceAlreadyInUseError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService -class DtDeviceArbitration(BaseService): +class DtDeviceArbitration(LockdownService): SERVICE_NAME = 'com.apple.dt.devicearbitration' def __init__(self, lockdown: LockdownClient): diff --git a/pymobiledevice3/services/diagnostics.py b/pymobiledevice3/services/diagnostics.py index bdc9473f7..7add14880 100755 --- a/pymobiledevice3/services/diagnostics.py +++ b/pymobiledevice3/services/diagnostics.py @@ -2,7 +2,8 @@ from pymobiledevice3.exceptions import ConnectionFailedError, PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService MobileGestaltKeys = ['BasebandKeyHashInformation', 'BasebandFirmwareManifestData', @@ -87,22 +88,27 @@ 'ApNonce'] -class DiagnosticsService(BaseService): +class DiagnosticsService(LockdownService): """ Provides an API to: * Query MobileGestalt & IORegistry keys. * Reboot, shutdown or put the device in sleep mode. """ - SERVICE_NAME_NEW = 'com.apple.mobile.diagnostics_relay' - SERVICE_NAME_OLD = 'com.apple.iosdiagnostics.relay' - - def __init__(self, lockdown: LockdownClient): - try: - service = lockdown.start_service(self.SERVICE_NAME_NEW) - service_name = self.SERVICE_NAME_NEW - except ConnectionFailedError: - service = lockdown.start_service(self.SERVICE_NAME_OLD) - service_name = self.SERVICE_NAME_OLD + RSD_SERVICE_NAME = 'com.apple.mobile.diagnostics_relay.shim.remote' + SERVICE_NAME = 'com.apple.mobile.diagnostics_relay' + OLD_SERVICE_NAME = 'com.apple.iosdiagnostics.relay' + + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + try: + service = lockdown.start_lockdown_service(self.SERVICE_NAME) + service_name = self.SERVICE_NAME + except ConnectionFailedError: + service = lockdown.start_lockdown_service(self.OLD_SERVICE_NAME) + service_name = self.OLD_SERVICE_NAME + else: + service = None + service_name = self.RSD_SERVICE_NAME super().__init__(lockdown, service_name, service=service) diff --git a/pymobiledevice3/services/dtfetchsymbols.py b/pymobiledevice3/services/dtfetchsymbols.py index 990be0fd1..6af75ea85 100755 --- a/pymobiledevice3/services/dtfetchsymbols.py +++ b/pymobiledevice3/services/dtfetchsymbols.py @@ -34,7 +34,7 @@ def get_file(self, fileno: int, stream: typing.IO): received += len(buf) def _start_command(self, cmd: bytes): - service = self.lockdown.start_developer_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_developer_service(self.SERVICE_NAME) service.sendall(cmd) # receive same command as an ack diff --git a/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py b/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py index f6b957dbe..a6a56efd5 100644 --- a/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +++ b/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py @@ -1,15 +1,22 @@ +from typing import Union + from packaging.version import Version from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService from pymobiledevice3.services.remote_server import RemoteServer class DvtSecureSocketProxyService(RemoteServer): SERVICE_NAME = 'com.apple.instruments.remoteserver.DVTSecureSocketProxy' OLD_SERVICE_NAME = 'com.apple.instruments.remoteserver' + RSD_SERVICE_NAME = 'com.apple.instruments.dtservicehub' - def __init__(self, lockdown: LockdownClient): - if Version(lockdown.product_version) >= Version('14.0'): + def __init__(self, lockdown: Union[RemoteServiceDiscoveryService, LockdownClient]): + if isinstance(lockdown, RemoteServiceDiscoveryService): + service_name = self.RSD_SERVICE_NAME + remove_ssl_context = False + elif Version(lockdown.product_version) >= Version('14.0'): service_name = self.SERVICE_NAME remove_ssl_context = False else: diff --git a/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py b/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py index 9602ed61f..5a71ade61 100644 --- a/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +++ b/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py @@ -9,6 +9,7 @@ from pykdebugparser.kd_buf_parser import RAW_VERSION2_BYTES from pymobiledevice3.exceptions import ExtractingStackshotError +from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.resources.dsc_uuid_map import get_dsc_map from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo @@ -676,8 +677,14 @@ def get_time_config(dvt): mach_absolute_time = time_info[0] numer = time_info[1] denom = time_info[2] - usecs_since_epoch = dvt.lockdown.get_value(key='TimeIntervalSince1970') * 1000000 + + usecs_since_epoch = None + timezone_ = None + if isinstance(dvt.lockdown, LockdownClient): + usecs_since_epoch = dvt.lockdown.get_value(key='TimeIntervalSince1970') * 1000000 + timezone_ = timezone(timedelta(seconds=dvt.lockdown.get_value(key='TimeZoneOffsetFromUTC'))) + return dict( numer=numer, denom=denom, mach_absolute_time=mach_absolute_time, usecs_since_epoch=usecs_since_epoch, - timezone=timezone(timedelta(seconds=dvt.lockdown.get_value(key='TimeZoneOffsetFromUTC'))) + timezone=timezone_ ) diff --git a/pymobiledevice3/services/dvt/instruments/device_info.py b/pymobiledevice3/services/dvt/instruments/device_info.py index 082a192ea..71de07b47 100644 --- a/pymobiledevice3/services/dvt/instruments/device_info.py +++ b/pymobiledevice3/services/dvt/instruments/device_info.py @@ -60,8 +60,10 @@ def mach_time_info(self): def mach_kernel_name(self) -> str: return self.request_information('machKernelName') - def kpep_database(self) -> typing.Mapping: - return plistlib.loads(self.request_information('kpepDatabase')) + def kpep_database(self) -> typing.Optional[typing.Mapping]: + kpep_database = self.request_information('kpepDatabase') + if kpep_database is not None: + return plistlib.loads(kpep_database) def trace_codes(self): codes_file = self.request_information('traceCodesFile') diff --git a/pymobiledevice3/services/file_relay.py b/pymobiledevice3/services/file_relay.py index 568f42afd..9dbc2374e 100755 --- a/pymobiledevice3/services/file_relay.py +++ b/pymobiledevice3/services/file_relay.py @@ -1,5 +1,5 @@ from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService SRCFILES = '''Baseband CrashReporter @@ -22,7 +22,7 @@ WirelessAutomation''' -class FileRelayService(BaseService): +class FileRelayService(LockdownService): SERVICE_NAME = 'com.apple.mobile.file_relay' def __init__(self, lockdown: LockdownClient): diff --git a/pymobiledevice3/services/heartbeat.py b/pymobiledevice3/services/heartbeat.py index 032c969c6..1430d6348 100644 --- a/pymobiledevice3/services/heartbeat.py +++ b/pymobiledevice3/services/heartbeat.py @@ -2,21 +2,26 @@ import time from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService -class HeartbeatService(BaseService): +class HeartbeatService(LockdownService): """ Use to keep an active connection with lockdowd """ SERVICE_NAME = 'com.apple.mobile.heartbeat' + RSD_SERVICE_NAME = 'com.apple.mobile.heartbeat.shim.remote' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def start(self, interval=None): start = time.time() - service = self.lockdown.start_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.SERVICE_NAME) while True: response = service.recv_plist() diff --git a/pymobiledevice3/services/house_arrest.py b/pymobiledevice3/services/house_arrest.py index 9c524444c..60b078f8f 100755 --- a/pymobiledevice3/services/house_arrest.py +++ b/pymobiledevice3/services/house_arrest.py @@ -1,17 +1,17 @@ -import logging - from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.services.afc import AfcService, AfcShell class HouseArrestService(AfcService): SERVICE_NAME = 'com.apple.mobile.house_arrest' + RSD_SERVICE_NAME = 'com.apple.mobile.house_arrest.shim.remote' - def __init__(self, lockdown: LockdownClient): - self.logger = logging.getLogger(__name__) - self.lockdown = lockdown - service_name = self.SERVICE_NAME - super(HouseArrestService, self).__init__(self.lockdown, service_name) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def send_command(self, bundle_id, cmd='VendContainer'): self.service.send_plist({'Command': cmd, 'Identifier': bundle_id}) diff --git a/pymobiledevice3/services/installation_proxy.py b/pymobiledevice3/services/installation_proxy.py index cb2e3af2f..7cd257fec 100644 --- a/pymobiledevice3/services/installation_proxy.py +++ b/pymobiledevice3/services/installation_proxy.py @@ -5,16 +5,20 @@ from pymobiledevice3.exceptions import AppInstallError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.afc import AfcService -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService GET_APPS_ADDITIONAL_INFO = {'ReturnAttributes': ['CFBundleIdentifier', 'StaticDiskUsage', 'DynamicDiskUsage']} -class InstallationProxyService(BaseService): +class InstallationProxyService(LockdownService): SERVICE_NAME = 'com.apple.mobile.installation_proxy' + RSD_SERVICE_NAME = 'com.apple.mobile.installation_proxy.shim.remote' def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def _watch_completion(self, handler: Callable = None, *args) -> None: while True: diff --git a/pymobiledevice3/services/base_service.py b/pymobiledevice3/services/lockdown_service.py similarity index 53% rename from pymobiledevice3/services/base_service.py rename to pymobiledevice3/services/lockdown_service.py index f61163974..d64e4600b 100644 --- a/pymobiledevice3/services/base_service.py +++ b/pymobiledevice3/services/lockdown_service.py @@ -1,21 +1,22 @@ import logging -from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.service_connection import LockdownServiceConnection -class BaseService: - def __init__(self, lockdown: LockdownClient, service_name: str, is_developer_service=False, - service: ServiceConnection = None): +class LockdownService: + def __init__(self, lockdown: LockdownServiceProvider, service_name: str, is_developer_service=False, + service: LockdownServiceConnection = None): """ - :param lockdown: lockdown connection + :param lockdown: server provider :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: - start_service = lockdown.start_developer_service if is_developer_service else lockdown.start_service + if service is None: + start_service = lockdown.start_lockdown_developer_service if is_developer_service else \ + lockdown.start_lockdown_service service = start_service(service_name) self.service_name = service_name @@ -29,5 +30,5 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close() - def close(self): + def close(self) -> None: self.service.close() diff --git a/pymobiledevice3/services/misagent.py b/pymobiledevice3/services/misagent.py index 8c323b6e6..a0cb0f287 100755 --- a/pymobiledevice3/services/misagent.py +++ b/pymobiledevice3/services/misagent.py @@ -1,10 +1,10 @@ import plistlib from io import BytesIO -from typing import List +from typing import List, Mapping from pymobiledevice3.exceptions import PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService class ProvisioningProfile: @@ -19,13 +19,17 @@ def __str__(self): return str(self.plist) -class MisagentService(BaseService): +class MisagentService(LockdownService): SERVICE_NAME = 'com.apple.misagent' + RSD_SERVICE_NAME = 'com.apple.misagent.shim.remote' def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) - def install(self, plist: BytesIO): + def install(self, plist: BytesIO) -> Mapping: response = self.service.send_recv_plist({'MessageType': 'Install', 'Profile': plist.read(), 'ProfileType': 'Provisioning'}) @@ -34,7 +38,7 @@ def install(self, plist: BytesIO): return response - def remove(self, profile_id): + def remove(self, profile_id: str) -> Mapping: response = self.service.send_recv_plist({'MessageType': 'Remove', 'ProfileID': profile_id, 'ProfileType': 'Provisioning'}) diff --git a/pymobiledevice3/services/mobile_activation.py b/pymobiledevice3/services/mobile_activation.py index 07bd7f960..0fe040035 100755 --- a/pymobiledevice3/services/mobile_activation.py +++ b/pymobiledevice3/services/mobile_activation.py @@ -72,14 +72,14 @@ def activate_with_session(self, activation_record, headers): } if headers: data['ActivationResponseHeaders'] = dict(headers) - with closing(create_using_usbmux(self.lockdown.udid).start_service(self.SERVICE_NAME)) as service: + with closing(create_using_usbmux(self.lockdown.udid).start_lockdown_service(self.SERVICE_NAME)) as service: return service.send_recv_plist(data) def send_command(self, command, value=''): data = {'Command': command} if value: data['Value'] = value - with closing(create_using_usbmux(self.lockdown.udid).start_service(self.SERVICE_NAME)) as service: + with closing(create_using_usbmux(self.lockdown.udid).start_lockdown_service(self.SERVICE_NAME)) as service: return service.send_recv_plist(data) def post(self, url, data, headers=None): diff --git a/pymobiledevice3/services/mobile_config.py b/pymobiledevice3/services/mobile_config.py index a2829f6e6..c1ec1482b 100755 --- a/pymobiledevice3/services/mobile_config.py +++ b/pymobiledevice3/services/mobile_config.py @@ -9,18 +9,22 @@ from pymobiledevice3.exceptions import ProfileError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService class Purpose(Enum): PostSetupInstallation = 'PostSetupInstallation' -class MobileConfigService(BaseService): +class MobileConfigService(LockdownService): SERVICE_NAME = 'com.apple.mobile.MCInstall' + RSD_SERVICE_NAME = 'com.apple.mobile.MCInstall.shim.remote' def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def hello(self) -> None: self._send_recv({'RequestType': 'HelloHostIdentifier'}) diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index a9f1d731f..bbdafe142 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -12,15 +12,19 @@ PyMobileDevice3Exception, UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.restore.tss import TSSRequest -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService -class MobileImageMounterService(BaseService): +class MobileImageMounterService(LockdownService): # implemented in /usr/libexec/mobile_storage_proxy SERVICE_NAME = 'com.apple.mobile.mobile_image_mounter' + RSD_SERVICE_NAME = 'com.apple.mobile.mobile_image_mounter.shim.remote' def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def copy_devices(self) -> List[Mapping]: """ Copy mounted devices list. """ @@ -197,7 +201,7 @@ def mount(self, image: Path, build_manifest: Path, trust_cache: Path, try: manifest = self.query_personalization_manifest('DeveloperDiskImage', hashlib.sha384(image).digest()) except MissingManifestError: - self.service = self.lockdown.start_service(self.SERVICE_NAME) + self.service = self.lockdown.start_lockdown_service(self.SERVICE_NAME) manifest = self.get_manifest_from_tss(plistlib.loads(build_manifest.read_bytes())) self.upload_image(self.IMAGE_TYPE, image, manifest) diff --git a/pymobiledevice3/services/mobilebackup2.py b/pymobiledevice3/services/mobilebackup2.py index 720a88d45..fad591c44 100755 --- a/pymobiledevice3/services/mobilebackup2.py +++ b/pymobiledevice3/services/mobilebackup2.py @@ -10,9 +10,9 @@ PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.afc import AFC_LOCK_EX, AFC_LOCK_UN, AfcService, afc_error_t -from pymobiledevice3.services.base_service import BaseService from pymobiledevice3.services.device_link import DeviceLink from pymobiledevice3.services.installation_proxy import InstallationProxyService +from pymobiledevice3.services.lockdown_service import LockdownService from pymobiledevice3.services.notification_proxy import NotificationProxyService from pymobiledevice3.services.springboard import SpringBoardServicesService @@ -28,7 +28,7 @@ NP_SYNC_DID_FINISH = 'com.apple.itunes-mobdev.syncDidFinish' -class Mobilebackup2Service(BaseService): +class Mobilebackup2Service(LockdownService): SERVICE_NAME = 'com.apple.mobilebackup2' def __init__(self, lockdown: LockdownClient): diff --git a/pymobiledevice3/services/notification_proxy.py b/pymobiledevice3/services/notification_proxy.py index 8213f68e3..da8da4f42 100755 --- a/pymobiledevice3/services/notification_proxy.py +++ b/pymobiledevice3/services/notification_proxy.py @@ -2,33 +2,42 @@ from typing import Generator, Mapping, Union from pymobiledevice3.exceptions import NotificationTimeoutError -from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.services.lockdown_service import LockdownService -class NotificationProxyService(BaseService): +class NotificationProxyService(LockdownService): SERVICE_NAME = 'com.apple.mobile.notification_proxy' + RSD_SERVICE_NAME = 'com.apple.mobile.notification_proxy.shim.remote' + INSECURE_SERVICE_NAME = 'com.apple.mobile.insecure_notification_proxy' + RSD_INSECURE_SERVICE_NAME = 'com.apple.mobile.insecure_notification_proxy.shim.remote' + + def __init__(self, lockdown: LockdownServiceProvider, insecure=False, timeout: Union[float, int] = None): + if isinstance(lockdown, RemoteServiceDiscoveryService): + secure_service_name = self.RSD_SERVICE_NAME + insecure_service_name = self.RSD_INSECURE_SERVICE_NAME + else: + secure_service_name = self.SERVICE_NAME + insecure_service_name = self.INSECURE_SERVICE_NAME - def __init__(self, lockdown: LockdownClient, insecure=False, timeout: Union[float, int] = None): if insecure: - super().__init__(lockdown, self.INSECURE_SERVICE_NAME) + super().__init__(lockdown, insecure_service_name) else: - super().__init__(lockdown, self.SERVICE_NAME) + super().__init__(lockdown, secure_service_name) if timeout is not None: self.service.socket.settimeout(timeout) def notify_post(self, name: str) -> None: """ Send notification to the device's notification_proxy. """ - self.service.send_plist({'Command': 'PostNotification', - 'Name': name}) + self.service.send_plist({'Command': 'PostNotification', 'Name': name}) def notify_register_dispatch(self, name: str) -> None: """ Tells the device to send a notification on the specified event. """ self.logger.info(f'Observing {name}') - self.service.send_plist({'Command': 'ObserveNotification', - 'Name': name}) + self.service.send_plist({'Command': 'ObserveNotification', 'Name': name}) def receive_notification(self) -> Generator[Mapping, None, None]: while True: diff --git a/pymobiledevice3/services/os_trace.py b/pymobiledevice3/services/os_trace.py index d0f8f92f8..5cb9d9fa8 100644 --- a/pymobiledevice3/services/os_trace.py +++ b/pymobiledevice3/services/os_trace.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import logging import plistlib import struct import tempfile @@ -11,7 +10,8 @@ from pymobiledevice3.exceptions import PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService from pymobiledevice3.utils import try_decode CHUNK_SIZE = 4096 @@ -62,7 +62,7 @@ def _encode(self, obj, context, path): ) -class OsTraceService(BaseService): +class OsTraceService(LockdownService): """ Provides API for the following operations: * Show process list (process name and pid) @@ -71,10 +71,13 @@ class OsTraceService(BaseService): * Archive contain the contents are the `/var/db/diagnostics` directory """ SERVICE_NAME = 'com.apple.os_trace_relay' + RSD_SERVICE_NAME = 'com.apple.os_trace_relay.shim.remote' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) - self.logger = logging.getLogger(__name__) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def get_pid_list(self): self.service.send_plist({'Request': 'PidList'}) diff --git a/pymobiledevice3/services/pcapd.py b/pymobiledevice3/services/pcapd.py index de7b805bf..4b03c9e57 100755 --- a/pymobiledevice3/services/pcapd.py +++ b/pymobiledevice3/services/pcapd.py @@ -8,7 +8,8 @@ from construct import Byte, Bytes, Container, CString, Int16ub, Int32ub, Int32ul, Padded, Seek, Struct, this from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService INTERFACE_NAMES = enum.Enum('InterfaceNames', names={ 'other': 1, @@ -320,16 +321,20 @@ ) -class PcapdService(BaseService): +class PcapdService(LockdownService): """ Starting iOS 5, apple added a remote virtual interface (RVI) facility that allows mirroring networks traffic from an iOS device. On macOS, the virtual interface can be enabled with the rvictl command. This script allows to use this service on other systems. """ + RSD_SERVICE_NAME = 'com.apple.pcapd.shim.remote' SERVICE_NAME = 'com.apple.pcapd' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def watch(self, packets_count: int = -1, process: str = None) -> Generator[Container, None, None]: packet_index = 0 diff --git a/pymobiledevice3/services/power_assertion.py b/pymobiledevice3/services/power_assertion.py index a81e361e9..e129ded4c 100755 --- a/pymobiledevice3/services/power_assertion.py +++ b/pymobiledevice3/services/power_assertion.py @@ -2,14 +2,19 @@ import contextlib from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService -class PowerAssertionService(BaseService): +class PowerAssertionService(LockdownService): + RSD_SERVICE_NAME = 'com.apple.mobile.assertion_agent.shim.remote' SERVICE_NAME = 'com.apple.mobile.assertion_agent' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) @contextlib.contextmanager def create_power_assertion(self, type_: str, name: str, timeout: int, details: str = None): diff --git a/pymobiledevice3/services/preboard.py b/pymobiledevice3/services/preboard.py index 66c164938..baf2d699c 100644 --- a/pymobiledevice3/services/preboard.py +++ b/pymobiledevice3/services/preboard.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService -class PreboardService(BaseService): +class PreboardService(LockdownService): + RSD_SERVICE_NAME = 'com.apple.preboardservice_v2.shim.remote' SERVICE_NAME = 'com.apple.preboardservice_v2' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def create_stashbag(self, manifest): return self.service.send_recv_plist({'Command': 'CreateStashbag', 'Manifest': manifest}) diff --git a/pymobiledevice3/services/remote_server.py b/pymobiledevice3/services/remote_server.py index 209afb388..9bf17a97b 100644 --- a/pymobiledevice3/services/remote_server.py +++ b/pymobiledevice3/services/remote_server.py @@ -12,8 +12,8 @@ from pygments import formatters, highlight, lexers from pymobiledevice3.exceptions import DvtException, UnrecognizedSelectorError -from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService SHELL_USAGE = ''' # This shell allows you to send messages to the DVTSecureSocketProxy and receive answers easily. @@ -187,7 +187,7 @@ def add_fragment(self, mheader, chunk): self._stream_packet_data = b'' -class RemoteServer(BaseService): +class RemoteServer(LockdownService): """ Wrapper to Apple's RemoteServer. This server exports several ObjC objects allowing calling their respective selectors. @@ -236,8 +236,9 @@ class RemoteServer(BaseService): INSTRUMENTS_MESSAGE_TYPE = 2 EXPECTS_REPLY_MASK = 0x1000 - def __init__(self, lockdown: LockdownClient, service_name, remove_ssl_context=True): - super().__init__(lockdown, service_name, is_developer_service=True) + def __init__(self, lockdown: LockdownServiceProvider, service_name, remove_ssl_context: bool = True, + is_developer_service: bool = True): + super().__init__(lockdown, service_name, is_developer_service=is_developer_service) if remove_ssl_context and hasattr(self.service.socket, '_sslobj'): self.service.socket._sslobj = None diff --git a/pymobiledevice3/services/screenshot.py b/pymobiledevice3/services/screenshot.py index fef014fc8..da3b4f87e 100755 --- a/pymobiledevice3/services/screenshot.py +++ b/pymobiledevice3/services/screenshot.py @@ -1,9 +1,9 @@ from pymobiledevice3.exceptions import PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService -class ScreenshotService(BaseService): +class ScreenshotService(LockdownService): SERVICE_NAME = 'com.apple.mobile.screenshotr' def __init__(self, lockdown: LockdownClient): diff --git a/pymobiledevice3/services/simulate_location.py b/pymobiledevice3/services/simulate_location.py index 50bf33ddf..4cc409699 100755 --- a/pymobiledevice3/services/simulate_location.py +++ b/pymobiledevice3/services/simulate_location.py @@ -4,10 +4,10 @@ import gpxpy from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.services.lockdown_service import LockdownService -class DtSimulateLocation(BaseService): +class DtSimulateLocation(LockdownService): SERVICE_NAME = 'com.apple.dt.simulatelocation' def __init__(self, lockdown: LockdownClient): @@ -15,12 +15,12 @@ def __init__(self, lockdown: LockdownClient): def clear(self): """ stop simulation """ - service = self.lockdown.start_developer_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_developer_service(self.SERVICE_NAME) service.sendall(struct.pack('>I', 1)) def set(self, latitude: float, longitude: float): """ stop simulation """ - service = self.lockdown.start_developer_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_developer_service(self.SERVICE_NAME) service.sendall(struct.pack('>I', 0)) latitude = str(latitude).encode() longitude = str(longitude).encode() diff --git a/pymobiledevice3/services/springboard.py b/pymobiledevice3/services/springboard.py index 935470893..2d0027e62 100644 --- a/pymobiledevice3/services/springboard.py +++ b/pymobiledevice3/services/springboard.py @@ -2,7 +2,8 @@ from enum import IntEnum from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService class InterfaceOrientation(IntEnum): @@ -12,11 +13,15 @@ class InterfaceOrientation(IntEnum): LANDSCAPE_HOME_TO_LEFT = 4 -class SpringBoardServicesService(BaseService): +class SpringBoardServicesService(LockdownService): + RSD_SERVICE_NAME = 'com.apple.springboardservices.shim.remote' SERVICE_NAME = 'com.apple.springboardservices' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def get_icon_state(self, format_version: str = '2'): cmd = {'command': 'getIconState'} diff --git a/pymobiledevice3/services/syslog.py b/pymobiledevice3/services/syslog.py index 78f1d7468..4135db917 100644 --- a/pymobiledevice3/services/syslog.py +++ b/pymobiledevice3/services/syslog.py @@ -1,5 +1,6 @@ from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.services.base_service import BaseService +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.services.lockdown_service import LockdownService from pymobiledevice3.utils import try_decode CHUNK_SIZE = 4096 @@ -7,15 +8,19 @@ SYSLOG_LINE_SPLITTER = b'\n\x00' -class SyslogService(BaseService): +class SyslogService(LockdownService): """ View system logs """ SERVICE_NAME = 'com.apple.syslog_relay' + RSD_SERVICE_NAME = 'com.apple.syslog_relay.shim.remote' - def __init__(self, lockdown: LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, lockdown: LockdownServiceProvider): + if isinstance(lockdown, LockdownClient): + super().__init__(lockdown, self.SERVICE_NAME) + else: + super().__init__(lockdown, self.RSD_SERVICE_NAME) def watch(self): buf = b'' diff --git a/pymobiledevice3/services/webinspector.py b/pymobiledevice3/services/webinspector.py index 4641caa73..3b97a65be 100644 --- a/pymobiledevice3/services/webinspector.py +++ b/pymobiledevice3/services/webinspector.py @@ -11,7 +11,8 @@ from pymobiledevice3.exceptions import LaunchingApplicationError, RemoteAutomationNotEnabledError, \ WebInspectorNotEnabledError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.service_connection import LockdownServiceConnection from pymobiledevice3.services.web_protocol.automation_session import AutomationSession from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol @@ -105,8 +106,9 @@ def from_application_dictionary(cls, app_dict) -> 'Application': class WebinspectorService: SERVICE_NAME = 'com.apple.webinspector' + RSD_SERVICE_NAME = 'com.apple.webinspector.shim.remote' - def __init__(self, lockdown: LockdownClient, loop=None): + def __init__(self, lockdown: LockdownServiceProvider, loop=None): if loop is None: try: loop = asyncio.get_running_loop() @@ -114,10 +116,16 @@ def __init__(self, lockdown: LockdownClient, loop=None): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) nest_asyncio.apply(loop) + + if isinstance(lockdown, LockdownClient): + self.service_name = self.SERVICE_NAME + else: + self.service_name = self.RSD_SERVICE_NAME + self.loop = loop self.logger = logging.getLogger(__name__) self.lockdown = lockdown - self.service: Optional[ServiceConnection] = None + self.service: Optional[LockdownServiceConnection] = None self.connection_id = str(uuid.uuid4()).upper() self.state = None self.connected_application = {} @@ -137,7 +145,7 @@ def __init__(self, lockdown: LockdownClient, loop=None): self._recv_task: Optional[asyncio.Task] = None def connect(self, timeout: Union[float, int] = None): - self.service = self.await_(self.lockdown.aio_start_service(self.SERVICE_NAME)) + self.service = self.await_(self.lockdown.aio_start_lockdown_service(self.service_name)) self.await_(self._report_identifier()) try: self._handle_recv(self.await_(asyncio.wait_for(self._recv_message(), timeout))) diff --git a/pymobiledevice3/tcp_forwarder.py b/pymobiledevice3/tcp_forwarder.py index f164ff108..ca1b2d5a7 100644 --- a/pymobiledevice3/tcp_forwarder.py +++ b/pymobiledevice3/tcp_forwarder.py @@ -6,7 +6,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError from pymobiledevice3.lockdown import create_using_usbmux -from pymobiledevice3.service_connection import ServiceConnection +from pymobiledevice3.service_connection import LockdownServiceConnection class TcpForwarder: @@ -120,7 +120,7 @@ def _handle_server_connection(self): if self.enable_ssl: # use the lockdown pairing record lockdown = create_using_usbmux(self.serial, connection_type=self.usbmux_connection_type) - service_connection = ServiceConnection.create_using_usbmux( + service_connection = LockdownServiceConnection.create_using_usbmux( self.serial, self.dst_port, connection_type=self.usbmux_connection_type) with lockdown.ssl_file() as ssl_file: From 13a2487f66282ccd72d75d230f78f30b94eee842 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:43:08 +0300 Subject: [PATCH 120/234] xpc_message: add `XpcDate` decoding --- pymobiledevice3/remote/xpc_message.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index c33e1166c..0f3c0abc5 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -1,4 +1,5 @@ import uuid +from datetime import datetime from typing import Any, List, Mapping from construct import Aligned, Array, Bytes, Const, CString, Default, Double, Enum, ExprAdapter, FlagsEnum, \ @@ -152,6 +153,11 @@ def _decode_xpc_data(xpc_object) -> bytes: return xpc_object.data +def _decode_xpc_date(xpc_object) -> datetime: + # Convert from nanoseconds to seconds + return datetime.fromtimestamp(xpc_object.data / 1000000000) + + def decode_xpc_object(xpc_object) -> Any: decoders = { XpcMessageType.DICTIONARY: _decode_xpc_dictionary, @@ -162,6 +168,7 @@ def decode_xpc_object(xpc_object) -> Any: XpcMessageType.UUID: _decode_xpc_uuid, XpcMessageType.STRING: _decode_xpc_string, XpcMessageType.DATA: _decode_xpc_data, + XpcMessageType.DATE: _decode_xpc_date, } decoder = decoders.get(xpc_object.type) if decoder is None: From f8f7f5cbb52fe3aa7f0d54cd4d1adad1ee6c307b Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:43:36 +0300 Subject: [PATCH 121/234] xpc_message: add `XpcFileTransfer` decoding --- pymobiledevice3/remote/xpc_message.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index 0f3c0abc5..ba6162f6b 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -1,3 +1,4 @@ +import dataclasses import uuid from datetime import datetime from typing import Any, List, Mapping @@ -69,6 +70,10 @@ 'count' / Hex(Int32ul), 'entries' / If(this.count > 0, Array(this.count, XpcDictionaryEntry)), )) +XpcFileTransfer = Struct( + 'msg_id' / Int64ul, + 'data' / LazyBound(lambda: XpcObject), +) XpcObject = Struct( 'type' / XpcMessageType, 'data' / Switch(this.type, { @@ -86,6 +91,7 @@ XpcMessageType.FD: XpcFd, XpcMessageType.SHMEM: XpcShmem, XpcMessageType.ARRAY: XpcArray, + XpcMessageType.FILE_TRANSFER: XpcFileTransfer, }, default=Probe(lookahead=1000)), ) XpcPayload = Struct( @@ -113,6 +119,11 @@ class XpcUInt64Type(int): pass +@dataclasses.dataclass +class FileTransferType: + transfer_size: int + + def _decode_xpc_dictionary(xpc_object) -> Mapping: if xpc_object.data.count == 0: return {} @@ -158,6 +169,10 @@ def _decode_xpc_date(xpc_object) -> datetime: return datetime.fromtimestamp(xpc_object.data / 1000000000) +def _decode_xpc_file_transfer(xpc_object) -> FileTransferType: + return FileTransferType(transfer_size=_decode_xpc_dictionary(xpc_object.data.data)['s']) + + def decode_xpc_object(xpc_object) -> Any: decoders = { XpcMessageType.DICTIONARY: _decode_xpc_dictionary, @@ -169,6 +184,7 @@ def decode_xpc_object(xpc_object) -> Any: XpcMessageType.STRING: _decode_xpc_string, XpcMessageType.DATA: _decode_xpc_data, XpcMessageType.DATE: _decode_xpc_date, + XpcMessageType.FILE_TRANSFER: _decode_xpc_file_transfer, } decoder = decoders.get(xpc_object.type) if decoder is None: From cc43968592ac3a4efef339fc2db83982cd0b3a50 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:43:53 +0300 Subject: [PATCH 122/234] xpc_message: add `XpcDouble` encoding --- pymobiledevice3/remote/xpc_message.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index ba6162f6b..7d42eb1dc 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -241,6 +241,13 @@ def _build_xpc_data(payload: bool) -> Mapping: } +def _build_xpc_double(payload: float) -> Mapping: + return { + 'type': XpcMessageType.DOUBLE, + 'data': payload, + } + + def _build_xpc_uint64(payload: XpcUInt64Type) -> Mapping: return { 'type': XpcMessageType.UINT64, @@ -263,6 +270,7 @@ def _build_xpc_object(payload: Any) -> Mapping: str: _build_xpc_string, bytes: _build_xpc_data, bytearray: _build_xpc_data, + float: _build_xpc_double, 'XpcUInt64Type': _build_xpc_uint64, 'XpcInt64Type': _build_xpc_int64, } From 17adf55ec4f97cda65d77527e2b0ff2ef4d8e73f Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:44:59 +0300 Subject: [PATCH 123/234] remotexpc: add `iter_file_chunks()` and `receive_file()` --- pymobiledevice3/remote/remotexpc.py | 56 +++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index 7c6ecd036..e1507ddf6 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -1,13 +1,13 @@ import socket from socket import create_connection -from typing import Mapping, Optional, Tuple +from typing import Generator, Mapping, Optional, Tuple from construct import StreamError from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame, RstStreamFrame, SettingsFrame, \ WindowUpdateFrame from pymobiledevice3.exceptions import StreamClosedError -from pymobiledevice3.remote.xpc_message import XpcWrapper, create_xpc_wrapper, decode_xpc_object +from pymobiledevice3.remote.xpc_message import XpcFlags, XpcWrapper, create_xpc_wrapper, decode_xpc_object # Extracted by sniffing `remoted` traffic via Wireshark DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS = 100 @@ -18,6 +18,7 @@ HTTP2_MAGIC = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' ROOT_CHANNEL = 1 +FILE_TRANSFER_CHANNEL = 2 REPLY_CHANNEL = 3 @@ -26,7 +27,7 @@ def __init__(self, address: Tuple[str, int]): self._previous_frame_data = b'' self.address = address self.sock: Optional[socket.socket] = None - self.next_message_id: Mapping[int: int] = {ROOT_CHANNEL: 0, REPLY_CHANNEL: 0} + self.next_message_id: Mapping[int: int] = {ROOT_CHANNEL: 0, FILE_TRANSFER_CHANNEL: 0, REPLY_CHANNEL: 0} self.peer_info = None def __enter__(self) -> 'RemoteXPCConnection': @@ -48,15 +49,24 @@ def send_request(self, data: Mapping, wanting_reply: bool = False) -> None: data, message_id=self.next_message_id[ROOT_CHANNEL], wanting_reply=wanting_reply) self.sock.sendall(DataFrame(stream_id=ROOT_CHANNEL, data=xpc_wrapper).serialize()) - def receive_response(self): + def iter_file_chunks(self, total_size: int) -> Generator[bytes, None, None]: + self._open_channel(FILE_TRANSFER_CHANNEL, XpcFlags.FILE_TX_STREAM_RESPONSE) + size = 0 + while size < total_size: + frame = self._receive_next_data_frame() + assert frame.stream_id == FILE_TRANSFER_CHANNEL + size += len(frame.data) + yield frame.data + + def receive_file(self, total_size: int) -> bytes: + buf = b'' + for chunk in self.iter_file_chunks(total_size): + buf += chunk + return buf + + def receive_response(self) -> Mapping: while True: - frame = self._receive_frame() - if isinstance(frame, GoAwayFrame): - raise StreamClosedError(f'Got {frame}') - if isinstance(frame, RstStreamFrame): - raise StreamClosedError(f'Got {frame}') - if not isinstance(frame, DataFrame): - continue + frame = self._receive_next_data_frame() try: xpc_message = XpcWrapper.parse(self._previous_frame_data + frame.data).message self._previous_frame_data = b'' @@ -90,19 +100,35 @@ def _do_handshake(self) -> None: self._send_frame(DataFrame(stream_id=ROOT_CHANNEL, data=XpcWrapper.build({'size': 0, 'flags': 0x0201, 'payload': None}))) self.next_message_id[ROOT_CHANNEL] += 1 - self._send_frame(HeadersFrame(stream_id=REPLY_CHANNEL, flags=['END_HEADERS'])) - self._send_frame( - DataFrame(stream_id=REPLY_CHANNEL, - data=XpcWrapper.build({'size': 0, 'flags': 0x00400001, 'payload': None}))) + self._open_channel(REPLY_CHANNEL, XpcFlags.INIT_HANDSHAKE) self.next_message_id[REPLY_CHANNEL] += 1 assert isinstance(self._receive_frame(), SettingsFrame) self._send_frame(SettingsFrame(flags=['ACK'])) + def _open_channel(self, stream_id: int, flags: int): + flags |= XpcFlags.ALWAYS_SET + self._send_frame(HeadersFrame(stream_id=stream_id, flags=['END_HEADERS'])) + self._send_frame( + DataFrame(stream_id=stream_id, data=XpcWrapper.build({'size': 0, 'flags': flags, 'payload': None}))) + def _send_frame(self, frame: Frame) -> None: self.sock.sendall(frame.serialize()) + def _receive_next_data_frame(self) -> DataFrame: + while True: + frame = self._receive_frame() + + if isinstance(frame, GoAwayFrame): + raise StreamClosedError(f'Got {frame}') + if isinstance(frame, RstStreamFrame): + raise StreamClosedError(f'Got {frame}') + if not isinstance(frame, DataFrame): + continue + + return frame + def _receive_frame(self) -> Frame: buf = self._recvall(FRAME_HEADER_SIZE) frame, additional_size = Frame.parse_frame_header(memoryview(buf)) From 53a10ea3e028b8f39ff77c890139d04d894c6103 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:38:29 +0300 Subject: [PATCH 124/234] remote: add `CoreDevice` services --- .../remote/core_device/__init__.py | 0 .../remote/core_device/app_service.py | 106 ++++++++++++++++++ .../remote/core_device/core_device_service.py | 34 ++++++ .../remote/core_device/device_info.py | 35 ++++++ .../remote/core_device/diagnostics_service.py | 19 ++++ pymobiledevice3/remote/remote_service.py | 26 +++++ 6 files changed, 220 insertions(+) create mode 100644 pymobiledevice3/remote/core_device/__init__.py create mode 100644 pymobiledevice3/remote/core_device/app_service.py create mode 100644 pymobiledevice3/remote/core_device/core_device_service.py create mode 100644 pymobiledevice3/remote/core_device/device_info.py create mode 100644 pymobiledevice3/remote/core_device/diagnostics_service.py create mode 100644 pymobiledevice3/remote/remote_service.py diff --git a/pymobiledevice3/remote/core_device/__init__.py b/pymobiledevice3/remote/core_device/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pymobiledevice3/remote/core_device/app_service.py b/pymobiledevice3/remote/core_device/app_service.py new file mode 100644 index 000000000..c8f9ef3f8 --- /dev/null +++ b/pymobiledevice3/remote/core_device/app_service.py @@ -0,0 +1,106 @@ +import plistlib +from typing import List, Mapping + +from pymobiledevice3.remote.core_device.core_device_service import CoreDeviceService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.remote.xpc_message import XpcInt64Type + + +class AppServiceService(CoreDeviceService): + """ + Manage applications + """ + + SERVICE_NAME = 'com.apple.coredevice.appservice' + + def __init__(self, rsd: RemoteServiceDiscoveryService): + super().__init__(rsd, self.SERVICE_NAME) + + def list_apps(self, include_app_clips: bool = True, include_removable_apps: bool = True, + include_hidden_apps: bool = True, include_internal_apps: bool = True, + include_default_apps: bool = True) -> List[Mapping]: + """ List applications """ + return self.invoke('com.apple.coredevice.feature.listapps', { + 'includeAppClips': include_app_clips, 'includeRemovableApps': include_removable_apps, + 'includeHiddenApps': include_hidden_apps, 'includeInternalApps': include_internal_apps, + 'includeDefaultApps': include_default_apps}) + + def list_processes(self) -> List[Mapping]: + """ List processes """ + return self.invoke('com.apple.coredevice.feature.listprocesses')['processTokens'] + + def list_roots(self) -> Mapping: + """ + List roots. + + Can only be performed on certain devices + """ + return self.invoke('com.apple.coredevice.feature.listroots', { + 'rootPoint': { + 'relative': '/' + }}) + + def spawn_executable(self, executable: str, arguments: List[str]) -> Mapping: + """ + Spawn given executable. + + Can only be performed on certain devices + """ + return self.invoke('com.apple.coredevice.feature.spawnexecutable', { + 'executableItem': { + 'url': { + '_0': { + 'relative': executable, + }, + } + }, + 'standardIOIdentifiers': {}, + 'options': { + 'arguments': arguments, + 'environmentVariables': {}, + 'standardIOUsesPseudoterminals': True, + 'startStopped': False, + 'user': { + 'shortName': 'short-name', + + }, + 'platformSpecificOptions': plistlib.dumps({}), + }, + }) + + def monitor_process_termination(self, pid: int) -> Mapping: + """ + Monitor process termination. + + Can only be performed on certain devices + """ + return self.invoke('com.apple.coredevice.feature.monitorprocesstermination', { + 'processToken': {'processIdentifier': XpcInt64Type(pid)}}) + + def uninstall_app(self, bundle_identifier: str) -> None: + """ + Uninstall given application by its bundle identifier + """ + self.invoke('com.apple.coredevice.feature.uninstallapp', {'bundleIdentifier': bundle_identifier}) + + def send_signal_to_process(self, pid: int, signal: int) -> Mapping: + """ + Send signal to given process by its pid + """ + return self.invoke('com.apple.coredevice.feature.sendsignaltoprocess', { + 'process': {'processIdentifier': XpcInt64Type(pid)}, + 'signal': XpcInt64Type(signal), + }) + + def fetch_icons(self, bundle_identifier: str, width: float, height: float, scale: float, + allow_placeholder: bool) -> Mapping: + """ + Fetch given application's icons + """ + return self.invoke('com.apple.coredevice.feature.fetchappicons', { + 'width': width, + 'height': height, + 'scale': scale, + 'allowPlaceholder': allow_placeholder, + 'bundleIdentifier': bundle_identifier + }) diff --git a/pymobiledevice3/remote/core_device/core_device_service.py b/pymobiledevice3/remote/core_device/core_device_service.py new file mode 100644 index 000000000..cec01b8b9 --- /dev/null +++ b/pymobiledevice3/remote/core_device/core_device_service.py @@ -0,0 +1,34 @@ +import uuid +from typing import Any, Mapping + +from pymobiledevice3.exceptions import CoreDeviceError +from pymobiledevice3.remote.remote_service import RemoteService +from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type + + +def _generate_core_device_version_dict(version: str) -> Mapping: + version_components = version.split('.') + return {'components': [XpcUInt64Type(component) for component in version_components], + 'originalComponentsCount': XpcInt64Type(len(version_components)), + 'stringValue': version} + + +CORE_DEVICE_VERSION = _generate_core_device_version_dict('325.3') + + +class CoreDeviceService(RemoteService): + def invoke(self, feature_identifier: str, input_: Mapping = None) -> Any: + if input_ is None: + input_ = {} + response = self.service.send_receive_request({ + 'CoreDevice.CoreDeviceDDIProtocolVersion': XpcInt64Type(0), + 'CoreDevice.action': {}, + 'CoreDevice.coreDeviceVersion': CORE_DEVICE_VERSION, + 'CoreDevice.deviceIdentifier': str(uuid.uuid4()), + 'CoreDevice.featureIdentifier': feature_identifier, + 'CoreDevice.input': input_, + 'CoreDevice.invocationIdentifier': str(uuid.uuid4())}) + output = response.get('CoreDevice.output') + if output is None: + raise CoreDeviceError(f'Failed to invoke: {feature_identifier}. Got error: {response}') + return output diff --git a/pymobiledevice3/remote/core_device/device_info.py b/pymobiledevice3/remote/core_device/device_info.py new file mode 100644 index 000000000..4ce849d2f --- /dev/null +++ b/pymobiledevice3/remote/core_device/device_info.py @@ -0,0 +1,35 @@ +from typing import List, Mapping + +from pymobiledevice3.remote.core_device.core_device_service import CoreDeviceService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService + + +class DeviceInfoService(CoreDeviceService): + """ + Query device information + """ + + SERVICE_NAME = 'com.apple.coredevice.deviceinfo' + + def __init__(self, rsd: RemoteServiceDiscoveryService): + super().__init__(rsd, self.SERVICE_NAME) + + def get_device_info(self) -> Mapping: + """ + Get device information + """ + return self.invoke('com.apple.coredevice.feature.getdeviceinfo', {}) + + def query_mobilegestalt(self, keys: List[str]) -> Mapping: + """ + Query MobileGestalt. + + Can only be performed to specific devices + """ + return self.invoke('com.apple.coredevice.feature.querymobilegestalt', {'keys': keys}) + + def get_lockstate(self) -> Mapping: + """ + Get lockstate + """ + return self.invoke('com.apple.coredevice.feature.getlockstate', {}) diff --git a/pymobiledevice3/remote/core_device/diagnostics_service.py b/pymobiledevice3/remote/core_device/diagnostics_service.py new file mode 100644 index 000000000..a4f59e83c --- /dev/null +++ b/pymobiledevice3/remote/core_device/diagnostics_service.py @@ -0,0 +1,19 @@ +from typing import Generator + +from pymobiledevice3.remote.core_device.core_device_service import CoreDeviceService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService + + +class DiagnosticsServiceService(CoreDeviceService): + """ + Obtain device diagnostics + """ + + SERVICE_NAME = 'com.apple.coredevice.diagnosticsservice' + + def __init__(self, rsd: RemoteServiceDiscoveryService): + super().__init__(rsd, self.SERVICE_NAME) + + def capture_sysdiagnose(self, is_dry_run: bool) -> Generator[bytes, None, None]: + response = self.invoke('com.apple.coredevice.feature.capturesysdiagnose', {'isDryRun': is_dry_run}) + return self.service.iter_file_chunks(response['fileTransfer']['expectedLength']) diff --git a/pymobiledevice3/remote/remote_service.py b/pymobiledevice3/remote/remote_service.py new file mode 100644 index 000000000..2eeff3117 --- /dev/null +++ b/pymobiledevice3/remote/remote_service.py @@ -0,0 +1,26 @@ +import logging +from typing import Optional + +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.remote.remotexpc import RemoteXPCConnection + + +class RemoteService: + def __init__(self, rsd: RemoteServiceDiscoveryService, service_name: str): + self.service_name = service_name + self.rsd = rsd + self.service: Optional[RemoteXPCConnection] = None + self.logger = logging.getLogger(self.__module__) + + def connect(self) -> None: + self.service = self.rsd.start_remote_service(self.service_name) + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self) -> None: + self.service.close() From 7e147089e487697105ff23182355d2385d469a31 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 20 Jul 2023 13:53:47 +0300 Subject: [PATCH 125/234] services: add `RestoreService` --- pymobiledevice3/services/restore_service.py | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 pymobiledevice3/services/restore_service.py diff --git a/pymobiledevice3/services/restore_service.py b/pymobiledevice3/services/restore_service.py new file mode 100755 index 000000000..9adbf8551 --- /dev/null +++ b/pymobiledevice3/services/restore_service.py @@ -0,0 +1,41 @@ +from typing import Mapping + +from pymobiledevice3.exceptions import PyMobileDevice3Exception +from pymobiledevice3.remote.remote_service import RemoteService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService + + +class RestoreService(RemoteService): + SERVICE_NAME = 'com.apple.RestoreRemoteServices.restoreserviced' + + def __init__(self, lockdown: RemoteServiceDiscoveryService): + super().__init__(lockdown, self.SERVICE_NAME) + + def delay_recovery_image(self) -> None: + """ + Set `delay-recovery-image` on devices of ProductType 0x1677b394. Otherwise, fail + """ + self.validate_command('delayrecoveryimage') + + def enter_recovery(self) -> None: + """ Enter recovery """ + self.validate_command('recovery') + + def reboot(self) -> None: + """ Reboot device """ + self.validate_command('reboot') + + def get_preflightinfo(self) -> Mapping: + """ Get preflight info """ + return self.service.send_receive_request({'command': 'getpreflightinfo'}) + + def get_nonces(self) -> Mapping: + """ Get ApNonce and SEPNonce """ + return self.service.send_receive_request({'command': 'getnonces'}) + + def validate_command(self, command: str) -> Mapping: + """ Execute command and validate result is `success` """ + response = self.service.send_receive_request({'command': command}) + if response.get('result') != 'success': + raise PyMobileDevice3Exception(f'request command: {command} failed with error: {response}') + return response From e1206d2a4c831d500273fbe1d90f29ebec3a095d Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 23 Jul 2023 15:33:41 +0300 Subject: [PATCH 126/234] cli: refactor argument `lockdown`->`service_provider` --- pymobiledevice3/cli/activation.py | 12 +- pymobiledevice3/cli/afc.py | 20 +-- pymobiledevice3/cli/amfi.py | 8 +- pymobiledevice3/cli/apps.py | 12 +- pymobiledevice3/cli/backup.py | 36 ++--- pymobiledevice3/cli/cli_common.py | 14 +- pymobiledevice3/cli/companion_proxy.py | 4 +- pymobiledevice3/cli/crash.py | 28 ++-- pymobiledevice3/cli/developer.py | 192 ++++++++++++------------- pymobiledevice3/cli/diagnostics.py | 32 ++--- pymobiledevice3/cli/lockdown.py | 62 ++++---- pymobiledevice3/cli/mounter.py | 52 +++---- pymobiledevice3/cli/notification.py | 12 +- pymobiledevice3/cli/pcap.py | 4 +- pymobiledevice3/cli/power_assertion.py | 4 +- pymobiledevice3/cli/processes.py | 8 +- pymobiledevice3/cli/profile.py | 28 ++-- pymobiledevice3/cli/provision.py | 22 +-- pymobiledevice3/cli/remote.py | 12 +- pymobiledevice3/cli/springboard.py | 16 +-- pymobiledevice3/cli/syslog.py | 12 +- pymobiledevice3/cli/webinspector.py | 20 +-- 22 files changed, 305 insertions(+), 305 deletions(-) diff --git a/pymobiledevice3/cli/activation.py b/pymobiledevice3/cli/activation.py index 85902c044..4b148e60e 100644 --- a/pymobiledevice3/cli/activation.py +++ b/pymobiledevice3/cli/activation.py @@ -18,22 +18,22 @@ def activation(): @activation.command(cls=Command) -def state(lockdown: LockdownClient): +def state(service_provider: LockdownClient): """ Get current activation state """ - print(MobileActivationService(lockdown).state) + print(MobileActivationService(service_provider).state) @activation.command(cls=Command) @click.option('--now', is_flag=True, help='when --offline is used, dont wait for next nonce cycle') -def activate(lockdown: LockdownClient, now): +def activate(service_provider: LockdownClient, now): """ Activate device """ - activation_service = MobileActivationService(lockdown) + activation_service = MobileActivationService(service_provider) if not now: activation_service.wait_for_activation_session() activation_service.activate() @activation.command(cls=Command) -def deactivate(lockdown: LockdownClient): +def deactivate(service_provider: LockdownClient): """ Deactivate device """ - MobileActivationService(lockdown).deactivate() + MobileActivationService(service_provider).deactivate() diff --git a/pymobiledevice3/cli/afc.py b/pymobiledevice3/cli/afc.py index 9f37bd65d..d439b2c2f 100644 --- a/pymobiledevice3/cli/afc.py +++ b/pymobiledevice3/cli/afc.py @@ -18,38 +18,38 @@ def afc(): @afc.command('shell', cls=Command) -def afc_shell(lockdown: LockdownClient): +def afc_shell(service_provider: LockdownClient): """ open an AFC shell rooted at /var/mobile/Media """ - AfcShell(lockdown=lockdown).cmdloop() + AfcShell(lockdown=service_provider).cmdloop() @afc.command('pull', cls=Command) @click.argument('remote_file', type=click.Path(exists=False)) @click.argument('local_file', type=click.File('wb')) -def afc_pull(lockdown: LockdownClient, remote_file, local_file): +def afc_pull(service_provider: LockdownClient, remote_file, local_file): """ pull remote file from /var/mobile/Media """ - local_file.write(AfcService(lockdown=lockdown).get_file_contents(remote_file)) + local_file.write(AfcService(lockdown=service_provider).get_file_contents(remote_file)) @afc.command('push', cls=Command) @click.argument('local_file', type=click.File('rb')) @click.argument('remote_file', type=click.Path(exists=False)) -def afc_push(lockdown: LockdownClient, local_file, remote_file): +def afc_push(service_provider: LockdownClient, local_file, remote_file): """ push local file into /var/mobile/Media """ - AfcService(lockdown=lockdown).set_file_contents(remote_file, local_file.read()) + AfcService(lockdown=service_provider).set_file_contents(remote_file, local_file.read()) @afc.command('ls', cls=Command) @click.argument('remote_file', type=click.Path(exists=False)) @click.option('-r', '--recursive', is_flag=True) -def afc_ls(lockdown: LockdownClient, remote_file, recursive): +def afc_ls(service_provider: LockdownClient, remote_file, recursive): """ perform a dirlist rooted at /var/mobile/Media """ - for path in AfcService(lockdown=lockdown).dirlist(remote_file, -1 if recursive else 1): + for path in AfcService(lockdown=service_provider).dirlist(remote_file, -1 if recursive else 1): print(path) @afc.command('rm', cls=Command) @click.argument('remote_file', type=click.Path(exists=False)) -def afc_rm(lockdown: LockdownClient, remote_file): +def afc_rm(service_provider: LockdownClient, remote_file): """ remove a file rooted at /var/mobile/Media """ - AfcService(lockdown=lockdown).rm(remote_file) + AfcService(lockdown=service_provider).rm(remote_file) diff --git a/pymobiledevice3/cli/amfi.py b/pymobiledevice3/cli/amfi.py index 1e6cf96d5..0c41399fb 100644 --- a/pymobiledevice3/cli/amfi.py +++ b/pymobiledevice3/cli/amfi.py @@ -22,13 +22,13 @@ def amfi(): @amfi.command(cls=Command) -def enable_developer_mode(lockdown: LockdownClient): +def enable_developer_mode(service_provider: LockdownClient): """ enable developer mode """ - AmfiService(lockdown).enable_developer_mode() + AmfiService(service_provider).enable_developer_mode() @amfi.command(cls=Command) @click.option('--color/--no-color', default=True) -def developer_mode_status(lockdown: LockdownClient, color): +def developer_mode_status(service_provider: LockdownClient, color): """ query developer mode status """ - print_json(lockdown.developer_mode_status, colored=color) + print_json(service_provider.developer_mode_status, colored=color) diff --git a/pymobiledevice3/cli/apps.py b/pymobiledevice3/cli/apps.py index af6babb48..1b33c88d7 100644 --- a/pymobiledevice3/cli/apps.py +++ b/pymobiledevice3/cli/apps.py @@ -23,7 +23,7 @@ def apps(): @click.option('-u', '--user', is_flag=True, help='include user apps') @click.option('-s', '--system', is_flag=True, help='include system apps') @click.option('--hidden', is_flag=True, help='include hidden apps') -def apps_list(lockdown: LockdownClient, color, user, system, hidden): +def apps_list(service_provider: LockdownClient, color, user, system, hidden): """ list installed apps """ app_types = [] if user: @@ -32,21 +32,21 @@ def apps_list(lockdown: LockdownClient, color, user, system, hidden): app_types.append('System') if hidden: app_types.append('Hidden') - print_json(InstallationProxyService(lockdown=lockdown).get_apps(app_types), colored=color) + print_json(InstallationProxyService(lockdown=service_provider).get_apps(app_types), colored=color) @apps.command('uninstall', cls=Command) @click.argument('bundle_id') -def uninstall(lockdown: LockdownClient, bundle_id): +def uninstall(service_provider: LockdownClient, bundle_id): """ uninstall app by given bundle_id """ - InstallationProxyService(lockdown=lockdown).uninstall(bundle_id) + InstallationProxyService(lockdown=service_provider).uninstall(bundle_id) @apps.command('install', cls=Command) @click.argument('ipa_path', type=click.Path(exists=True)) -def install(lockdown: LockdownClient, ipa_path): +def install(service_provider: LockdownClient, ipa_path): """ install given .ipa """ - InstallationProxyService(lockdown=lockdown).install_from_local(ipa_path) + InstallationProxyService(lockdown=service_provider).install_from_local(ipa_path) @apps.command('afc', cls=Command) diff --git a/pymobiledevice3/cli/backup.py b/pymobiledevice3/cli/backup.py index 56934925f..35531e58f 100644 --- a/pymobiledevice3/cli/backup.py +++ b/pymobiledevice3/cli/backup.py @@ -32,13 +32,13 @@ def backup2(): @click.argument('backup-directory', type=click.Path(file_okay=False)) @click.option('--full', is_flag=True, help=('Whether to do a full backup.' ' If full is True, any previous backup attempts will be discarded.')) -def backup(lockdown: LockdownClient, backup_directory, full): +def backup(service_provider: LockdownClient, backup_directory, full): """ Backup device. All backup data will be written to BACKUP_DIRECTORY, under a directory named with the device's udid. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) with tqdm(total=100, dynamic_ncols=True) as pbar: def update_bar(percentage): pbar.n = percentage @@ -56,13 +56,13 @@ def update_bar(percentage): @click.option('--remove/--no-remove', default=False, help='Remove items which aren\'t being restored.') @password_option @source_option -def restore(lockdown: LockdownClient, backup_directory, system, reboot, copy, settings, remove, password, source): +def restore(service_provider: LockdownClient, backup_directory, system, reboot, copy, settings, remove, password, source): """ Restore a backup to a device. The backup will be restored from a directory with the device udid under BACKUP_DIRECTORY. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) with tqdm(total=100, dynamic_ncols=True) as pbar: def update_bar(percentage): pbar.n = percentage @@ -76,22 +76,22 @@ def update_bar(percentage): @backup2.command(cls=Command) @backup_directory_arg @source_option -def info(lockdown: LockdownClient, backup_directory, source): +def info(service_provider: LockdownClient, backup_directory, source): """ Print information about a backup. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) print(backup_client.info(backup_directory=backup_directory, source=source)) @backup2.command('list', cls=Command) @backup_directory_arg @source_option -def list_(lockdown: LockdownClient, backup_directory, source): +def list_(service_provider: LockdownClient, backup_directory, source): """ List all file in the backup in a CSV format. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) print(backup_client.list(backup_directory=backup_directory, source=source)) @@ -99,11 +99,11 @@ def list_(lockdown: LockdownClient, backup_directory, source): @backup_directory_arg @password_option @source_option -def unback(lockdown: LockdownClient, backup_directory, password, source): +def unback(service_provider: LockdownClient, backup_directory, password, source): """ Convert all files in the backup to the correct directory hierarchy. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) backup_client.unback(backup_directory=backup_directory, password=password, source=source) @@ -113,14 +113,14 @@ def unback(lockdown: LockdownClient, backup_directory, password, source): @backup_directory_arg @password_option @source_option -def extract(lockdown: LockdownClient, domain_name, relative_path, backup_directory, password, source): +def extract(service_provider: LockdownClient, domain_name, relative_path, backup_directory, password, source): """ Extract a file from the backup. The file that belongs to the domain DOMAIN_NAME and located on the device in the path RELATIVE_PATH, will be extracted to the BACKUP_DIRECTORY. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) backup_client.extract(domain_name, relative_path, backup_directory=backup_directory, password=password, source=source) @@ -129,14 +129,14 @@ def extract(lockdown: LockdownClient, domain_name, relative_path, backup_directo @click.argument('mode', type=click.Choice(['on', 'off'], case_sensitive=False)) @click.argument('password') @backup_directory_option -def encryption(lockdown: LockdownClient, backup_directory, mode, password): +def encryption(service_provider: LockdownClient, backup_directory, mode, password): """ Set backup encryption on / off. When on, PASSWORD will be the new backup password. When off, PASSWORD is the current backup password. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) should_encrypt = mode.lower() == 'on' if should_encrypt == backup_client.will_encrypt: logger.error('Encryption already ' + ('on!' if should_encrypt else 'off!')) @@ -151,11 +151,11 @@ def encryption(lockdown: LockdownClient, backup_directory, mode, password): @click.argument('old-password') @click.argument('new-password') @backup_directory_option -def change_password(lockdown: LockdownClient, old_password, new_password, backup_directory): +def change_password(service_provider: LockdownClient, old_password, new_password, backup_directory): """ Change the backup password. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) if not backup_client.will_encrypt: logger.error('Encryption is not turned on!') return @@ -164,9 +164,9 @@ def change_password(lockdown: LockdownClient, old_password, new_password, backup @backup2.command(cls=Command) @backup_directory_arg -def erase_device(lockdown: LockdownClient, backup_directory): +def erase_device(service_provider: LockdownClient, backup_directory): """ Erase all data on the device. """ - backup_client = Mobilebackup2Service(lockdown) + backup_client = Mobilebackup2Service(service_provider) backup_client.erase_device(backup_directory) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index f8e275f39..dda44abca 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -70,28 +70,28 @@ 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, + click.Option(('service_provider', '--rsd'), type=(str, int), callback=self.rsd, help='RSD hostname and port number'), - click.Option(('lockdown', '--udid'), envvar=UDID_ENV_VAR, callback=self.udid, + click.Option(('service_provider', '--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.lockdown_service_provider = None + self.service_provider = None def rsd(self, ctx, param: str, value: Optional[Tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]: if value is not None: with RemoteServiceDiscoveryService(value) as rsd: - self.lockdown_service_provider = rsd - return self.lockdown_service_provider + 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.lockdown_service_provider is not None: - return self.lockdown_service_provider + if self.service_provider is not None: + return self.service_provider if value is not None: return create_using_usbmux(serial=value) diff --git a/pymobiledevice3/cli/companion_proxy.py b/pymobiledevice3/cli/companion_proxy.py index 26971f50d..d19845bf4 100644 --- a/pymobiledevice3/cli/companion_proxy.py +++ b/pymobiledevice3/cli/companion_proxy.py @@ -19,6 +19,6 @@ def companion(): @companion.command('list', cls=Command) @click.option('--color/--no-color', default=True) -def companion_list(lockdown: LockdownClient, color): +def companion_list(service_provider: LockdownClient, color): """ list all paired companion devices """ - print_json(CompanionProxyService(lockdown).list(), colored=color, default=lambda x: '') + print_json(CompanionProxyService(service_provider).list(), colored=color, default=lambda x: '') diff --git a/pymobiledevice3/cli/crash.py b/pymobiledevice3/cli/crash.py index ea3f8ee41..8fa8c4811 100644 --- a/pymobiledevice3/cli/crash.py +++ b/pymobiledevice3/cli/crash.py @@ -19,9 +19,9 @@ def crash(): @crash.command('clear', cls=Command) @click.option('-f', '--flush', is_flag=True, default=False, help='flush before clear') -def crash_clear(lockdown: LockdownClient, flush): +def crash_clear(service_provider: LockdownClient, flush): """ clear(/remove) all crash reports """ - crash_manager = CrashReportsManager(lockdown) + crash_manager = CrashReportsManager(service_provider) if flush: crash_manager.flush() crash_manager.clear() @@ -31,49 +31,49 @@ def crash_clear(lockdown: LockdownClient, flush): @click.argument('out', type=click.Path(file_okay=False)) @click.argument('remote_file', type=click.Path(), required=False) @click.option('-e', '--erase', is_flag=True) -def crash_pull(lockdown: LockdownClient, out, remote_file, erase): +def crash_pull(service_provider: LockdownClient, out, remote_file, erase): """ pull all crash reports """ if remote_file is None: remote_file = '/' - CrashReportsManager(lockdown).pull(out, remote_file, erase) + CrashReportsManager(service_provider).pull(out, remote_file, erase) @crash.command('shell', cls=Command) -def crash_shell(lockdown: LockdownClient): +def crash_shell(service_provider: LockdownClient): """ start an afc shell """ - CrashReportsShell(lockdown=lockdown).cmdloop() + CrashReportsShell(lockdown=service_provider).cmdloop() @crash.command('ls', cls=Command) @click.argument('remote_file', type=click.Path(), required=False) @click.option('-d', '--depth', type=click.INT, default=1) -def crash_ls(lockdown: LockdownClient, remote_file, depth): +def crash_ls(service_provider: LockdownClient, remote_file, depth): """ List """ if remote_file is None: remote_file = '/' - for path in CrashReportsManager(lockdown).ls(remote_file, depth): + for path in CrashReportsManager(service_provider).ls(remote_file, depth): print(path) @crash.command('flush', cls=Command) -def crash_mover_flush(lockdown: LockdownClient): +def crash_mover_flush(service_provider: LockdownClient): """ trigger com.apple.crashreportmover to flush all products into CrashReports directory """ - CrashReportsManager(lockdown).flush() + CrashReportsManager(service_provider).flush() @crash.command('watch', cls=Command) @click.argument('name', required=False) @click.option('-r', '--raw', is_flag=True) -def crash_mover_watch(lockdown: LockdownClient, name, raw): +def crash_mover_watch(service_provider: LockdownClient, name, raw): """ watch for crash report generation """ - for crash_report in CrashReportsManager(lockdown).watch(name=name, raw=raw): + for crash_report in CrashReportsManager(service_provider).watch(name=name, raw=raw): print(crash_report) @crash.command('sysdiagnose', cls=Command) @click.argument('out', type=click.Path(exists=False, dir_okay=False, file_okay=True)) @click.option('-e', '--erase', is_flag=True, help='erase file after pulling') -def crash_sysdiagnose(lockdown: LockdownClient, out, erase): +def crash_sysdiagnose(service_provider: LockdownClient, out, erase): """ get a sysdiagnose archive from device (requires user interaction) """ print('Press Power+VolUp+VolDown for 0.215 seconds') - CrashReportsManager(lockdown).get_new_sysdiagnose(out, erase=erase) + CrashReportsManager(service_provider).get_new_sysdiagnose(out, erase=erase) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index b2daf1b80..44ebd902f 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -73,9 +73,9 @@ def developer(): @developer.command('shell', cls=Command) @click.argument('service') @click.option('-r', '--remove-ssl-context', is_flag=True) -def developer_shell(lockdown: LockdownClient, service, remove_ssl_context): +def developer_shell(service_provider: LockdownClient, service, remove_ssl_context): """ Launch developer shell. """ - with RemoteServer(lockdown, service, remove_ssl_context) as service: + with RemoteServer(service_provider, service, remove_ssl_context) as service: service.shell() @@ -87,9 +87,9 @@ def dvt(): @dvt.command('proclist', cls=Command) @click.option('--color/--no-color', default=True) -def proclist(lockdown: LockdownClient, color): +def proclist(service_provider: LockdownClient, color): """ show process list """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: processes = DeviceInfo(dvt).proclist() for process in processes: if 'startDate' in process: @@ -100,9 +100,9 @@ def proclist(lockdown: LockdownClient, color): @dvt.command('applist', cls=Command) @click.option('--color/--no-color', default=True) -def applist(lockdown: LockdownClient, color): +def applist(service_provider: LockdownClient, color): """ show application list """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: apps = ApplicationListing(dvt).applist() print_json(apps, colored=color) @@ -111,36 +111,36 @@ def applist(lockdown: LockdownClient, color): @click.argument('pid', type=click.INT) @click.argument('sig', type=click.INT, required=False) @click.option('-s', '--signal-name', type=click.Choice([s.name for s in signal.Signals])) -def send_signal(lockdown, pid, sig, signal_name): +def send_signal(service_provider, pid, sig, signal_name): """ Send SIGNAL to process by its PID """ if not sig and not signal_name: raise MissingParameter(param_type='argument|option', param_hint='\'SIG|SIGNAL-NAME\'') if sig and signal_name: raise UsageError(message='Cannot give SIG and SIGNAL-NAME together') sig = sig or signal.Signals[signal_name].value - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: ProcessControl(dvt).signal(pid, sig) @dvt.command('kill', cls=Command) @click.argument('pid', type=click.INT) -def kill(lockdown: LockdownClient, pid): +def kill(service_provider: LockdownClient, pid): """ Kill a process by its pid. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: ProcessControl(dvt).kill(pid) @dvt.command('pkill', cls=Command) @click.argument('expression') -def pkill(lockdown: LockdownClient, expression): +def pkill(service_provider: LockdownClient, expression): """ kill all processes containing `expression` in their name. """ - processes = OsTraceService(lockdown=lockdown).get_pid_list()['Payload'] + processes = OsTraceService(lockdown=service_provider).get_pid_list()['Payload'] if len(processes) == 0: # no point at trying to use DvtSecureSocketProxyService if no processes # were matched return - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: process_control = ProcessControl(dvt) for pid, process_info in processes.items(): process_name = process_info['ProcessName'] @@ -156,9 +156,9 @@ def pkill(lockdown: LockdownClient, expression): @click.option('--suspended', is_flag=True, help='Same as WaitForDebugger') @click.option('--env', multiple=True, type=click.Tuple((str, str)), help='Environment variables to pass to process given as a list of key value') -def launch(lockdown: LockdownClient, arguments: str, kill_existing: bool, suspended: bool, env: tuple): +def launch(service_provider: LockdownClient, arguments: str, kill_existing: bool, suspended: bool, env: tuple): """ Launch a process. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: parsed_arguments = shlex.split(arguments) pid = ProcessControl(dvt).launch(bundle_id=parsed_arguments[0], arguments=parsed_arguments[1:], kill_existing=kill_existing, start_suspended=suspended, @@ -167,9 +167,9 @@ def launch(lockdown: LockdownClient, arguments: str, kill_existing: bool, suspen @dvt.command('shell', cls=Command) -def dvt_shell(lockdown: LockdownClient): +def dvt_shell(service_provider: LockdownClient): """ Launch developer shell. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: dvt.shell() @@ -189,17 +189,17 @@ def show_dirlist(device_info: DeviceInfo, dirname, recursive=False): @dvt.command('ls', cls=Command) @click.argument('path', type=click.Path(exists=False, readable=False)) @click.option('-r', '--recursive', is_flag=True) -def ls(lockdown: LockdownClient, path, recursive): +def ls(service_provider: LockdownClient, path, recursive): """ List directory. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: show_dirlist(DeviceInfo(dvt), path, recursive=recursive) @dvt.command('device-information', cls=Command) @click.option('--color/--no-color', default=True) -def device_information(lockdown: LockdownClient, color): +def device_information(service_provider: LockdownClient, color): """ Print system information. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: device_info = DeviceInfo(dvt) info = { 'hardware': device_info.hardware_information(), @@ -215,9 +215,9 @@ def device_information(lockdown: LockdownClient, color): @dvt.command('netstat', cls=Command) -def netstat(lockdown: LockdownClient): +def netstat(service_provider: LockdownClient): """ Print information about current network activity. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with NetworkMonitor(dvt) as monitor: for event in monitor: if isinstance(event, ConnectionDetectionEvent): @@ -228,9 +228,9 @@ def netstat(lockdown: LockdownClient): @dvt.command('screenshot', cls=Command) @click.argument('out', type=click.File('wb')) -def screenshot(lockdown: LockdownClient, out): +def screenshot(service_provider: LockdownClient, out): """ get device screenshot """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: out.write(Screenshot(dvt).get_screenshot()) @@ -246,12 +246,12 @@ def sysmon_process(): @sysmon_process.command('monitor', cls=Command) @click.argument('threshold', type=click.FLOAT) -def sysmon_process_monitor(lockdown: LockdownClient, threshold): +def sysmon_process_monitor(service_provider: LockdownClient, threshold): """ monitor all most consuming processes by given cpuUsage threshold. """ Process = namedtuple('process', 'pid name cpuUsage') - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with Sysmontap(dvt) as sysmon: for process_snapshot in sysmon.iter_processes(): entries = [] @@ -266,13 +266,13 @@ def sysmon_process_monitor(lockdown: LockdownClient, threshold): @click.option('-a', '--attributes', multiple=True, help='filter processes by given attribute value given as key=value') @click.option('--color/--no-color', default=True) -def sysmon_process_single(lockdown: LockdownClient, attributes: List[str], color: bool): +def sysmon_process_single(service_provider: LockdownClient, attributes: List[str], color: bool): """ show a single snapshot of currently running processes. """ count = 0 result = [] - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: device_info = DeviceInfo(dvt) with Sysmontap(dvt) as sysmon: @@ -306,13 +306,13 @@ def sysmon_process_single(lockdown: LockdownClient, attributes: List[str], color @sysmon.command('system', cls=Command) @click.option('-f', '--fields', help='field names splitted by ",".') -def sysmon_system(lockdown: LockdownClient, fields): +def sysmon_system(service_provider: LockdownClient, fields): """ show current system stats. """ if fields is not None: fields = fields.split(',') - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: sysmontap = Sysmontap(dvt) with sysmontap as sysmon: for row in sysmon: @@ -365,7 +365,7 @@ def parse_filters(subclasses: List[int], classes: List[int]): @click.option('--show-tid/--no-show-tid', default=True, help='Whether to print thread id or not.') @click.option('--process-name/--no-process-name', default=True, help='Whether to print process name or not.') @click.option('--args/--no-args', default=True, help='Whether to print event arguments or not.') -def live_profile_session(lockdown: LockdownClient, count, bsc, class_filters, subclass_filters, tid, timestamp, +def live_profile_session(service_provider: LockdownClient, count, bsc, class_filters, subclass_filters, tid, timestamp, event_name, func_qual, show_tid, process_name, args): """ Print kevents received from the device in real time. """ @@ -382,7 +382,7 @@ def live_profile_session(lockdown: LockdownClient, count, bsc, class_filters, su parser.show_tid = show_tid parser.show_process = process_name parser.show_args = args - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: trace_codes_map = DeviceInfo(dvt).trace_codes() time_config = CoreProfileSessionTap.get_time_config(dvt) parser.numer = time_config['numer'] @@ -404,12 +404,12 @@ def live_profile_session(lockdown: LockdownClient, count, bsc, class_filters, su @bsc_filter @class_filter @subclass_filter -def save_profile_session(lockdown: LockdownClient, out, bsc, class_filters, subclass_filters): +def save_profile_session(service_provider: LockdownClient, out, bsc, class_filters, subclass_filters): """ Dump core profiling information. """ if bsc: subclass_filters = list(subclass_filters) + [BSC_SUBCLASS] filters = parse_filters(subclass_filters, class_filters) - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with CoreProfileSessionTap(dvt, {}, filters) as tap: tap.dump(out) @@ -417,9 +417,9 @@ def save_profile_session(lockdown: LockdownClient, out, bsc, class_filters, subc @core_profile_session.command('stackshot', cls=Command) @click.option('--out', type=click.File('w'), default=None) @click.option('--color/--no-color', default=True) -def stackshot(lockdown: LockdownClient, out, color): +def stackshot(service_provider: LockdownClient, out, color): """ Dump stackshot information. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with CoreProfileSessionTap(dvt, {}) as tap: try: data = tap.get_stackshot() @@ -442,10 +442,10 @@ def stackshot(lockdown: LockdownClient, out, color): @subclass_filter @click.option('--process', default=None, help='Process ID / name to filter. Omit for all.') @click.option('--color/--no-color', default=True) -def parse_live_profile_session(lockdown: LockdownClient, count, tid, show_tid, bsc, class_filters, subclass_filters, +def parse_live_profile_session(service_provider: LockdownClient, count, tid, show_tid, bsc, class_filters, subclass_filters, process, color): """ Print traces (syscalls, thread events, etc.) received from the device in real time. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: print('Receiving time information') time_config = CoreProfileSessionTap.get_time_config(dvt) parser = PyKdebugParser() @@ -502,9 +502,9 @@ def format_callstack(callstack, dsc_uuid_map, current_dsc_map): @click.option('--tid', type=click.INT, default=None, help='Thread ID to filter. Omit for all.') @click.option('--show-tid/--no-show-tid', default=False, help='Whether to print thread id or not.') @click.option('--color/--no-color', default=True) -def callstacks_live_profile_session(lockdown: LockdownClient, count, process, tid, show_tid, color): +def callstacks_live_profile_session(service_provider: LockdownClient, count, process, tid, show_tid, color): """ Print callstacks received from the device in real time. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: print('Receiving time information') time_config = CoreProfileSessionTap.get_time_config(dvt) parser = PyKdebugParser() @@ -533,27 +533,27 @@ def callstacks_live_profile_session(lockdown: LockdownClient, count, process, ti @dvt.command('trace-codes', cls=Command) @click.option('--color/--no-color', default=True) -def dvt_trace_codes(lockdown: LockdownClient, color): +def dvt_trace_codes(service_provider: LockdownClient, color): """ Print KDebug trace codes. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: device_info = DeviceInfo(dvt) print_json({hex(k): v for k, v in device_info.trace_codes().items()}, colored=color) @dvt.command('name-for-uid', cls=Command) @click.argument('uid', type=click.INT) -def dvt_name_for_uid(lockdown: LockdownClient, uid): +def dvt_name_for_uid(service_provider: LockdownClient, uid): """ Print the assiciated username for the given uid. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: device_info = DeviceInfo(dvt) print(device_info.name_for_uid(uid)) @dvt.command('name-for-gid', cls=Command) @click.argument('gid', type=click.INT) -def dvt_name_for_gid(lockdown: LockdownClient, gid): +def dvt_name_for_gid(service_provider: LockdownClient, gid): """ Print the assiciated group name for the given gid. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: device_info = DeviceInfo(dvt) print(device_info.name_for_gid(gid)) @@ -561,9 +561,9 @@ def dvt_name_for_gid(lockdown: LockdownClient, gid): @dvt.command('oslog', cls=Command) @click.option('--color/--no-color', default=True) @click.option('--pid', type=click.INT) -def dvt_oslog(lockdown: LockdownClient, color, pid): +def dvt_oslog(service_provider: LockdownClient, color, pid): """ oslog. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with ActivityTraceTap(dvt) as tap: for message in tap: message_pid = message.process @@ -598,7 +598,7 @@ def dvt_oslog(lockdown: LockdownClient, color, pid): @dvt.command('energy', cls=Command) @click.argument('pid-list', nargs=-1) -def dvt_energy(lockdown: LockdownClient, pid_list): +def dvt_energy(service_provider: LockdownClient, pid_list): """ energy monitoring for given pid list. """ if len(pid_list) == 0: @@ -607,25 +607,25 @@ def dvt_energy(lockdown: LockdownClient, pid_list): pid_list = [int(pid) for pid in pid_list] - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with EnergyMonitor(dvt, pid_list) as energy_monitor: for telemetry in energy_monitor: logger.info(telemetry) @dvt.command('notifications', cls=Command) -def dvt_notifications(lockdown: LockdownClient): +def dvt_notifications(service_provider: LockdownClient): """ monitor memory and app notifications """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with Notifications(dvt) as notifications: for notification in notifications: logger.info(notification) @dvt.command('graphics', cls=Command) -def dvt_notifications(lockdown: LockdownClient): +def dvt_notifications(service_provider: LockdownClient): """ monitor graphics statistics """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: with Graphics(dvt) as graphics: for stats in graphics: logger.info(stats) @@ -639,16 +639,16 @@ def fetch_symbols(): @fetch_symbols.command('list', cls=Command) @click.option('--color/--no-color', default=True) -def fetch_symbols_list(lockdown: LockdownClient, color: bool): +def fetch_symbols_list(service_provider: LockdownClient, color: bool): """ list of files to be downloaded """ - print_json(DtFetchSymbols(lockdown).list_files(), colored=color) + print_json(DtFetchSymbols(service_provider).list_files(), colored=color) @fetch_symbols.command('download', cls=Command) @click.argument('out', type=click.Path(dir_okay=True, file_okay=False)) -def fetch_symbols_download(lockdown: LockdownClient, out): +def fetch_symbols_download(service_provider: LockdownClient, out): """ download the linker and dyld cache to a specified directory """ - fetch_symbols = DtFetchSymbols(lockdown) + fetch_symbols = DtFetchSymbols(service_provider) files = fetch_symbols.list_files() out = Path(out) @@ -682,31 +682,31 @@ def simulate_location(): @simulate_location.command('clear', cls=Command) -def simulate_location_clear(lockdown: LockdownClient): +def simulate_location_clear(service_provider: LockdownClient): """ clear simulated location """ - DtSimulateLocation(lockdown).clear() + DtSimulateLocation(service_provider).clear() @simulate_location.command('set', cls=Command) @click.argument('latitude', type=click.FLOAT) @click.argument('longitude', type=click.FLOAT) -def simulate_location_set(lockdown: LockdownClient, latitude, longitude): +def simulate_location_set(service_provider: LockdownClient, latitude, longitude): """ set a simulated location. try: ... set -- 40.690008 -74.045843 for liberty island """ - DtSimulateLocation(lockdown).set(latitude, longitude) + DtSimulateLocation(service_provider).set(latitude, longitude) @simulate_location.command('play', cls=Command) @click.argument('filename', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @click.option('--disable-sleep', is_flag=True, default=False) -def simulate_location_play(lockdown: LockdownClient, filename, disable_sleep): +def simulate_location_play(service_provider: LockdownClient, filename, disable_sleep): """ play a .gpx file """ - DtSimulateLocation(lockdown).play_gpx_file(filename, disable_sleep=disable_sleep) + DtSimulateLocation(service_provider).play_gpx_file(filename, disable_sleep=disable_sleep) @developer.group('accessibility') @@ -716,9 +716,9 @@ def accessibility(): @accessibility.command('capabilities', cls=Command) -def accessibility_capabilities(lockdown: LockdownClient): +def accessibility_capabilities(service_provider: LockdownClient): """ display accessibility capabilities """ - print_json(AccessibilityAudit(lockdown).capabilities) + print_json(AccessibilityAudit(service_provider).capabilities) @accessibility.group('settings') @@ -728,37 +728,37 @@ def accessibility_settings(): @accessibility_settings.command('show', cls=Command) -def accessibility_settings_show(lockdown: LockdownClient): +def accessibility_settings_show(service_provider: LockdownClient): """ show current settings """ - for setting in AccessibilityAudit(lockdown).settings: + for setting in AccessibilityAudit(service_provider).settings: print(setting) @accessibility_settings.command('set', cls=Command) @click.argument('setting') @click.argument('value') -def accessibility_settings_set(lockdown: LockdownClient, setting, value): +def accessibility_settings_set(service_provider: LockdownClient, setting, value): """ change current settings in order to list all available use the "show" command """ - service = AccessibilityAudit(lockdown) + service = AccessibilityAudit(service_provider) service.set_setting(setting, eval(value)) wait_return() @accessibility.command('shell', cls=Command) -def accessibility_shell(lockdown: LockdownClient): +def accessibility_shell(service_provider: LockdownClient): """ start and ipython accessibility shell """ - AccessibilityAudit(lockdown).shell() + AccessibilityAudit(service_provider).shell() @accessibility.command('notifications', cls=Command) -def accessibility_notifications(lockdown: LockdownClient): +def accessibility_notifications(service_provider: LockdownClient): """ show notifications """ - service = AccessibilityAudit(lockdown) + service = AccessibilityAudit(service_provider) for event in service.iter_events(): if event.name in ('hostAppStateChanged:', 'hostInspectorCurrentElementChanged:',): @@ -767,10 +767,10 @@ def accessibility_notifications(lockdown: LockdownClient): @accessibility.command('list-items', cls=Command) -def accessibility_list_items(lockdown: LockdownClient): +def accessibility_list_items(service_provider: LockdownClient): """ list items available in currently shown menu """ - service = AccessibilityAudit(lockdown) + service = AccessibilityAudit(service_provider) iterator = service.iter_events() # every focus change is expected publish a "hostInspectorCurrentElementChanged:" @@ -810,26 +810,26 @@ def condition_list(lockdown: LockdownClient): @condition.command('clear', cls=Command) -def condition_clear(lockdown: LockdownClient): +def condition_clear(service_provider: LockdownClient): """ clear current condition """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: ConditionInducer(dvt).clear() @condition.command('set', cls=Command) @click.argument('profile_identifier') -def condition_set(lockdown: LockdownClient, profile_identifier): +def condition_set(service_provider: LockdownClient, profile_identifier): """ set a specific condition """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: ConditionInducer(dvt).set(profile_identifier) wait_return() @developer.command(cls=Command) @click.argument('out', type=click.File('wb')) -def screenshot(lockdown: LockdownClient, out): +def screenshot(service_provider: LockdownClient, out): """ take a screenshot in PNG format """ - out.write(ScreenshotService(lockdown=lockdown).take_screenshot()) + out.write(ScreenshotService(lockdown=service_provider).take_screenshot()) @developer.group('debugserver') @@ -839,14 +839,14 @@ def debugserver(): @debugserver.command('applist', cls=Command) -def debugserver_applist(lockdown: LockdownClient): +def debugserver_applist(service_provider: LockdownClient): """ get applist xml """ - print_json(DebugServerAppList(lockdown).get()) + print_json(DebugServerAppList(service_provider).get()) @debugserver.command('start-server', cls=Command) @click.argument('local_port', type=click.INT) -def debugserver_start_server(lockdown: LockdownClient, local_port): +def debugserver_start_server(service_provider: LockdownClient, local_port): """ start a debugserver at remote listening on a given port locally. @@ -857,8 +857,8 @@ def debugserver_start_server(lockdown: LockdownClient, local_port): (lldb) platform connect connect://localhost: """ - attr = lockdown.get_service_connection_attributes('com.apple.debugserver.DVTSecureSocketProxy') - TcpForwarder(local_port, attr['Port'], serial=lockdown.identifier, + attr = service_provider.get_service_connection_attributes('com.apple.debugserver.DVTSecureSocketProxy') + TcpForwarder(local_port, attr['Port'], serial=service_provider.identifier, enable_ssl=attr.get('EnableServiceSSL', False)).start() @@ -870,18 +870,18 @@ def arbitration(): @arbitration.command('version', cls=Command) @click.option('--color/--no-color', default=True) -def version(lockdown: LockdownClient, color): +def version(service_provider: LockdownClient, color): """ get arbitration version """ - with DtDeviceArbitration(lockdown) as device_arbitration: + with DtDeviceArbitration(service_provider) as device_arbitration: print_json(device_arbitration.version, colored=color) @arbitration.command('check-in', cls=Command) @click.argument('hostname') @click.option('-f', '--force', default=False, is_flag=True) -def check_in(lockdown: LockdownClient, hostname, force): +def check_in(service_provider: LockdownClient, hostname, force): """ owner check-in """ - with DtDeviceArbitration(lockdown) as device_arbitration: + with DtDeviceArbitration(service_provider) as device_arbitration: try: device_arbitration.check_in(hostname, force=force) wait_return() @@ -890,16 +890,16 @@ def check_in(lockdown: LockdownClient, hostname, force): @arbitration.command('check-out', cls=Command) -def check_out(lockdown: LockdownClient): +def check_out(service_provider: LockdownClient): """ owner check-out """ - with DtDeviceArbitration(lockdown) as device_arbitration: + with DtDeviceArbitration(service_provider) as device_arbitration: device_arbitration.check_out() @dvt.command('har', cls=Command) -def dvt_har(lockdown: LockdownClient): +def dvt_har(service_provider: LockdownClient): """ enable har-logging """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: print('> Press Ctrl-C to abort') with ActivityTraceTap(dvt, enable_http_archive_logging=True) as tap: while True: diff --git a/pymobiledevice3/cli/diagnostics.py b/pymobiledevice3/cli/diagnostics.py index ddf54a47f..7037b7ec5 100644 --- a/pymobiledevice3/cli/diagnostics.py +++ b/pymobiledevice3/cli/diagnostics.py @@ -23,28 +23,28 @@ def diagnostics(): @diagnostics.command('restart', cls=Command) -def diagnostics_restart(lockdown: LockdownClient): +def diagnostics_restart(service_provider: LockdownClient): """ restart device """ - DiagnosticsService(lockdown=lockdown).restart() + DiagnosticsService(lockdown=service_provider).restart() @diagnostics.command('shutdown', cls=Command) -def diagnostics_shutdown(lockdown: LockdownClient): +def diagnostics_shutdown(service_provider: LockdownClient): """ shutdown device """ - DiagnosticsService(lockdown=lockdown).shutdown() + DiagnosticsService(lockdown=service_provider).shutdown() @diagnostics.command('sleep', cls=Command) -def diagnostics_sleep(lockdown: LockdownClient): +def diagnostics_sleep(service_provider: LockdownClient): """ put device into sleep """ - DiagnosticsService(lockdown=lockdown).sleep() + DiagnosticsService(lockdown=service_provider).sleep() @diagnostics.command('info', cls=Command) @click.option('--color/--no-color', default=True) -def diagnostics_info(lockdown: LockdownClient, color): +def diagnostics_info(service_provider: LockdownClient, color): """ get diagnostics info """ - print_json(DiagnosticsService(lockdown=lockdown).info(), colored=color) + print_json(DiagnosticsService(lockdown=service_provider).info(), colored=color) @diagnostics.command('ioregistry', cls=Command) @@ -52,17 +52,17 @@ def diagnostics_info(lockdown: LockdownClient, color): @click.option('--name') @click.option('--ioclass') @click.option('--color/--no-color', default=True) -def diagnostics_ioregistry(lockdown: LockdownClient, plane, name, ioclass, color): +def diagnostics_ioregistry(service_provider: LockdownClient, plane, name, ioclass, color): """ get ioregistry info """ - print_json(DiagnosticsService(lockdown=lockdown).ioregistry(plane=plane, name=name, ioclass=ioclass), colored=color) + print_json(DiagnosticsService(lockdown=service_provider).ioregistry(plane=plane, name=name, ioclass=ioclass), colored=color) @diagnostics.command('mg', cls=Command) @click.argument('keys', nargs=-1, default=None) @click.option('--color/--no-color', default=True) -def diagnostics_mg(lockdown: LockdownClient, keys, color): +def diagnostics_mg(service_provider: LockdownClient, keys, color): """ get MobileGestalt key values from given list. If empty, return all known. """ - print_json(DiagnosticsService(lockdown=lockdown).mobilegestalt(keys=keys), colored=color) + print_json(DiagnosticsService(lockdown=service_provider).mobilegestalt(keys=keys), colored=color) @diagnostics.group('battery') @@ -73,16 +73,16 @@ def diagnostics_battery(): @diagnostics_battery.command('single', cls=Command) @click.option('--color/--no-color', default=True) -def diagnostics_battery_single(lockdown: LockdownClient, color): +def diagnostics_battery_single(service_provider: LockdownClient, color): """ get single snapshot of battery data """ - raw_info = DiagnosticsService(lockdown=lockdown).get_battery() + raw_info = DiagnosticsService(lockdown=service_provider).get_battery() print_json(raw_info, colored=color) @diagnostics_battery.command('monitor', cls=Command) -def diagnostics_battery_monitor(lockdown: LockdownClient): +def diagnostics_battery_monitor(service_provider: LockdownClient): """ monitor battery usage """ - diagnostics = DiagnosticsService(lockdown=lockdown) + diagnostics = DiagnosticsService(lockdown=service_provider) while True: raw_info = diagnostics.get_battery() info = { diff --git a/pymobiledevice3/cli/lockdown.py b/pymobiledevice3/cli/lockdown.py index e4e054dfa..dd0382384 100644 --- a/pymobiledevice3/cli/lockdown.py +++ b/pymobiledevice3/cli/lockdown.py @@ -23,33 +23,33 @@ def lockdown_group(): @lockdown_group.command('recovery', cls=Command) -def lockdown_recovery(lockdown: LockdownClient): +def lockdown_recovery(service_provider: LockdownClient): """ enter recovery """ - print_json(lockdown.enter_recovery()) + print_json(service_provider.enter_recovery()) @lockdown_group.command('service', cls=Command) @click.argument('service_name') -def lockdown_service(lockdown: LockdownClient, service_name): +def lockdown_service(service_provider: LockdownClient, service_name): """ send-receive raw service messages """ - lockdown.start_lockdown_service(service_name).shell() + service_provider.start_lockdown_service(service_name).shell() @lockdown_group.command('info', cls=Command) @click.option('-a', '--all', is_flag=True, help='include all domain information') @click.option('--color/--no-color', default=True) -def lockdown_info(lockdown: LockdownClient, all, color): +def lockdown_info(service_provider: LockdownClient, all, color): """ query all lockdown values """ - print_json(lockdown.all_domains if all else lockdown.all_values, colored=color) + print_json(service_provider.all_domains if all else service_provider.all_values, colored=color) @lockdown_group.command('get', cls=Command) @click.argument('domain', required=False) @click.argument('key', required=False) @click.option('--color/--no-color', default=True) -def lockdown_get(lockdown: LockdownClient, domain, key, color): +def lockdown_get(service_provider: LockdownClient, domain, key, color): """ query lockdown values by their domain and key names """ - print_json(lockdown.get_value(domain=domain, key=key), colored=color) + print_json(service_provider.get_value(domain=domain, key=key), colored=color) @lockdown_group.command('set', cls=Command) @@ -57,78 +57,78 @@ def lockdown_get(lockdown: LockdownClient, domain, key, color): @click.argument('domain', required=False) @click.argument('key', required=False) @click.option('--color/--no-color', default=True) -def lockdown_set(lockdown: LockdownClient, value, domain, key, color): +def lockdown_set(service_provider: LockdownClient, value, domain, key, color): """ set a lockdown value using python's eval() """ - print_json(lockdown.set_value(value=eval(value), domain=domain, key=key), colored=color) + print_json(service_provider.set_value(value=eval(value), domain=domain, key=key), colored=color) @lockdown_group.command('remove', cls=Command) @click.argument('domain') @click.argument('key') @click.option('--color/--no-color', default=True) -def lockdown_remove(lockdown: LockdownClient, domain, key, color): +def lockdown_remove(service_provider: LockdownClient, domain, key, color): """ remove a domain/key pair """ - print_json(lockdown.remove_value(domain=domain, key=key), colored=color) + print_json(service_provider.remove_value(domain=domain, key=key), colored=color) @lockdown_group.command('unpair', cls=CommandWithoutAutopair) @click.argument('host_id', required=False) -def lockdown_unpair(lockdown: LockdownClient, host_id: str = None): +def lockdown_unpair(service_provider: LockdownClient, host_id: str = None): """ unpair from connected device """ - lockdown.unpair(host_id=host_id) + service_provider.unpair(host_id=host_id) @lockdown_group.command('pair', cls=CommandWithoutAutopair) -def lockdown_pair(lockdown: LockdownClient): +def lockdown_pair(service_provider: LockdownClient): """ pair device """ - lockdown.pair() + service_provider.pair() @lockdown_group.command('save-pair-record', cls=CommandWithoutAutopair) @click.argument('output', type=click.File('wb')) -def lockdown_save_pair_record(lockdown: LockdownClient, output): +def lockdown_save_pair_record(service_provider: LockdownClient, output): """ save pair record to specified location """ - if lockdown.pair_record is None: + if service_provider.pair_record is None: logger.error('no pairing record was found') return - plistlib.dump(lockdown.pair_record, output) + plistlib.dump(service_provider.pair_record, output) @lockdown_group.command('date', cls=Command) -def lockdown_date(lockdown: LockdownClient): +def lockdown_date(service_provider: LockdownClient): """ get device date """ - print(lockdown.date) + print(service_provider.date) @lockdown_group.command('heartbeat', cls=Command) -def lockdown_heartbeat(lockdown: LockdownClient): +def lockdown_heartbeat(service_provider: LockdownClient): """ start heartbeat service """ - HeartbeatService(lockdown).start() + HeartbeatService(service_provider).start() @lockdown_group.command('language', cls=Command) -def lockdown_language(lockdown: LockdownClient): +def lockdown_language(service_provider: LockdownClient): """ get current language settings """ - print(f'{lockdown.language} {lockdown.locale}') + print(f'{service_provider.language} {service_provider.locale}') @lockdown_group.command('device-name', cls=Command) @click.argument('new_name', required=False) -def lockdown_device_name(lockdown: LockdownClient, new_name): +def lockdown_device_name(service_provider: LockdownClient, new_name): """ get/set current device name """ if new_name: - lockdown.set_value(new_name, key='DeviceName') + service_provider.set_value(new_name, key='DeviceName') else: - print(f'{lockdown.get_value(key="DeviceName")}') + print(f'{service_provider.get_value(key="DeviceName")}') @lockdown_group.command('wifi-connections', cls=Command) @click.argument('state', type=click.Choice(['on', 'off']), required=False) -def lockdown_wifi_connections(lockdown: LockdownClient, state): +def lockdown_wifi_connections(service_provider: LockdownClient, state): """ get/set wifi connections state """ if not state: # show current state - print_json(lockdown.get_value(domain='com.apple.mobile.wireless_lockdown')) + print_json(service_provider.get_value(domain='com.apple.mobile.wireless_lockdown')) else: # enable/disable - lockdown.enable_wifi_connections = state == 'on' + service_provider.enable_wifi_connections = state == 'on' diff --git a/pymobiledevice3/cli/mounter.py b/pymobiledevice3/cli/mounter.py index 4cee00b6f..e999fc3fd 100644 --- a/pymobiledevice3/cli/mounter.py +++ b/pymobiledevice3/cli/mounter.py @@ -41,11 +41,11 @@ def mounter(): @mounter.command('list', cls=Command) @click.option('--color/--no-color', default=True) -def mounter_list(lockdown: LockdownClient, color): +def mounter_list(service_provider: LockdownClient, color): """ list all mounted images """ output = [] - images = MobileImageMounterService(lockdown=lockdown).copy_devices() + images = MobileImageMounterService(lockdown=service_provider).copy_devices() for image in images: image_signature = image.get('ImageSignature') if image_signature is not None: @@ -58,10 +58,10 @@ def mounter_list(lockdown: LockdownClient, color): @mounter.command('lookup', cls=Command) @click.option('--color/--no-color', default=True) @click.argument('image_type') -def mounter_lookup(lockdown: LockdownClient, color, image_type): +def mounter_lookup(service_provider: LockdownClient, color, image_type): """ lookup mounter image type """ try: - signature = MobileImageMounterService(lockdown=lockdown).lookup_image(image_type) + signature = MobileImageMounterService(lockdown=service_provider).lookup_image(image_type) print_json(signature, colored=color) except NotMountedError: logger.error(f'Disk image of type: {image_type} is not mounted') @@ -69,10 +69,10 @@ def mounter_lookup(lockdown: LockdownClient, color, image_type): @mounter.command('umount-developer', cls=Command) @catch_errors -def mounter_umount_developer(lockdown: LockdownClient): +def mounter_umount_developer(service_provider: LockdownClient): """ unmount Developer image """ try: - DeveloperDiskImageMounter(lockdown=lockdown).umount() + DeveloperDiskImageMounter(lockdown=service_provider).umount() logger.info('Developer image unmounted successfully') except NotMountedError: logger.error('Developer image isn\'t currently mounted') @@ -80,10 +80,10 @@ def mounter_umount_developer(lockdown: LockdownClient): @mounter.command('umount-personalized', cls=Command) @catch_errors -def mounter_umount_personalized(lockdown: LockdownClient): +def mounter_umount_personalized(service_provider: LockdownClient): """ unmount Personalized image """ try: - PersonalizedImageMounter(lockdown=lockdown).umount() + PersonalizedImageMounter(lockdown=service_provider).umount() logger.info('Personalized image unmounted successfully') except NotMountedError: logger.error('Personalized image isn\'t currently mounted') @@ -93,9 +93,9 @@ def mounter_umount_personalized(lockdown: LockdownClient): @click.argument('image', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @click.argument('signature', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @catch_errors -def mounter_mount_developer(lockdown: LockdownClient, image: str, signature: str): +def mounter_mount_developer(service_provider: LockdownClient, image: str, signature: str): """ mount developer image """ - DeveloperDiskImageMounter(lockdown=lockdown).mount(Path(image), Path(signature)) + DeveloperDiskImageMounter(lockdown=service_provider).mount(Path(image), Path(signature)) logger.info('Developer image mounted successfully') @@ -104,9 +104,9 @@ def mounter_mount_developer(lockdown: LockdownClient, image: str, signature: str @click.argument('trust-cache', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @click.argument('build-manifest', type=click.Path(exists=True, file_okay=True, dir_okay=False)) @catch_errors -def mounter_mount_personalized(lockdown: LockdownClient, image: str, trust_cache: str, build_manifest: str): +def mounter_mount_personalized(service_provider: LockdownClient, image: str, trust_cache: str, build_manifest: str): """ mount personalized image """ - PersonalizedImageMounter(lockdown=lockdown).mount(Path(image), Path(build_manifest), Path(trust_cache)) + PersonalizedImageMounter(lockdown=service_provider).mount(Path(image), Path(build_manifest), Path(trust_cache)) logger.info('Personalized image mounted successfully') @@ -115,10 +115,10 @@ def mounter_mount_personalized(lockdown: LockdownClient, image: str, trust_cache help='Xcode application path used to figure out automatically the DeveloperDiskImage path') @click.option('-v', '--version', help='use a different DeveloperDiskImage version from the one retrieved by lockdown' 'connection') -def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): +def mounter_auto_mount(service_provider: LockdownClient, xcode: str, version: str): """ auto-detect correct DeveloperDiskImage and mount it """ try: - auto_mount(lockdown, xcode=xcode, version=version) + auto_mount(service_provider, xcode=xcode, version=version) logger.info('DeveloperDiskImage mounted successfully') except URLError: logger.warning('failed to query DeveloperDiskImage versions') @@ -134,43 +134,43 @@ def mounter_auto_mount(lockdown: LockdownClient, xcode: str, version: str): @mounter.command('query-developer-mode-status', cls=Command) @click.option('--color/--no-color', default=True) -def mounter_query_developer_mode_status(lockdown: LockdownClient, color): +def mounter_query_developer_mode_status(service_provider: LockdownClient, color): """ Query developer mode status """ - print_json(MobileImageMounterService(lockdown=lockdown).query_developer_mode_status(), colored=color) + print_json(MobileImageMounterService(lockdown=service_provider).query_developer_mode_status(), colored=color) @mounter.command('query-nonce', cls=Command) @click.option('--image-type') @click.option('--color/--no-color', default=True) -def mounter_query_nonce(lockdown: LockdownClient, image_type: str, color: bool): +def mounter_query_nonce(service_provider: LockdownClient, image_type: str, color: bool): """ Query nonce """ - print_json(MobileImageMounterService(lockdown=lockdown).query_nonce(image_type), colored=color) + print_json(MobileImageMounterService(lockdown=service_provider).query_nonce(image_type), colored=color) @mounter.command('query-personalization-identifiers', cls=Command) @click.option('--color/--no-color', default=True) -def mounter_query_personalization_identifiers(lockdown: LockdownClient, color): +def mounter_query_personalization_identifiers(service_provider: LockdownClient, color): """ Query personalization identifiers """ - print_json(MobileImageMounterService(lockdown=lockdown).query_personalization_identifiers(), colored=color) + print_json(MobileImageMounterService(lockdown=service_provider).query_personalization_identifiers(), colored=color) @mounter.command('query-personalization-manifest', cls=Command) @click.option('--color/--no-color', default=True) -def mounter_query_personalization_manifest(lockdown: LockdownClient, color): +def mounter_query_personalization_manifest(service_provider: LockdownClient, color): """ Query personalization manifest """ result = [] - mounter = MobileImageMounterService(lockdown=lockdown) + mounter = MobileImageMounterService(lockdown=service_provider) for device in mounter.copy_devices(): result.append(mounter.query_personalization_manifest(device['PersonalizedImageType'], device['ImageSignature'])) print_json(result, colored=color) @mounter.command('roll-personalization-nonce', cls=Command) -def mounter_roll_personalization_nonce(lockdown: LockdownClient): - MobileImageMounterService(lockdown=lockdown).roll_personalization_nonce() +def mounter_roll_personalization_nonce(service_provider: LockdownClient): + MobileImageMounterService(lockdown=service_provider).roll_personalization_nonce() @mounter.command('roll-cryptex-nonce', cls=Command) -def mounter_roll_cryptex_nonce(lockdown: LockdownClient): +def mounter_roll_cryptex_nonce(service_provider: LockdownClient): """ Roll cryptex nonce (will reboot) """ - MobileImageMounterService(lockdown=lockdown).roll_cryptex_nonce() + MobileImageMounterService(lockdown=service_provider).roll_cryptex_nonce() diff --git a/pymobiledevice3/cli/notification.py b/pymobiledevice3/cli/notification.py index 190b3626f..68a29127e 100644 --- a/pymobiledevice3/cli/notification.py +++ b/pymobiledevice3/cli/notification.py @@ -25,9 +25,9 @@ def notification(): @notification.command(cls=Command) @click.argument('names', nargs=-1) @click.option('--insecure', is_flag=True, help='use the insecure relay meant for untrusted clients instead') -def post(lockdown: LockdownClient, names, insecure): +def post(service_provider: LockdownClient, names, insecure): """ API for notify_post(). """ - service = NotificationProxyService(lockdown=lockdown, insecure=insecure) + service = NotificationProxyService(lockdown=service_provider, insecure=insecure) for name in names: service.notify_post(name) @@ -35,9 +35,9 @@ def post(lockdown: LockdownClient, names, insecure): @notification.command(cls=Command) @click.argument('names', nargs=-1) @click.option('--insecure', is_flag=True, help='use the insecure relay meant for untrusted clients instead') -def observe(lockdown: LockdownClient, names, insecure): +def observe(service_provider: LockdownClient, names, insecure): """ API for notify_register_dispatch(). """ - service = NotificationProxyService(lockdown=lockdown, insecure=insecure) + service = NotificationProxyService(lockdown=service_provider, insecure=insecure) for name in names: service.notify_register_dispatch(name) @@ -47,9 +47,9 @@ def observe(lockdown: LockdownClient, names, insecure): @notification.command('observe-all', cls=Command) @click.option('--insecure', is_flag=True, help='use the insecure relay meant for untrusted clients instead') -def observe_all(lockdown: LockdownClient, insecure): +def observe_all(service_provider: LockdownClient, insecure): """ attempt to observe all builtin firmware notifications. """ - service = NotificationProxyService(lockdown=lockdown, insecure=insecure) + service = NotificationProxyService(lockdown=service_provider, insecure=insecure) for notification in get_notifications(): service.notify_register_dispatch(notification) diff --git a/pymobiledevice3/cli/pcap.py b/pymobiledevice3/cli/pcap.py index e68df3557..77efac601 100644 --- a/pymobiledevice3/cli/pcap.py +++ b/pymobiledevice3/cli/pcap.py @@ -41,9 +41,9 @@ def print_packet(packet, color: bool): @click.option('-c', '--count', type=click.INT, default=-1, help='Number of packets to sniff. Omit to endless sniff.') @click.option('--process', default=None, help='Process to filter. Omit for all.') @click.option('--color/--no-color', default=True) -def pcap(lockdown: LockdownClient, out: IO, count: int, process: str, color: bool): +def pcap(service_provider: LockdownClient, out: IO, count: int, process: str, color: bool): """ sniff device traffic """ - service = PcapdService(lockdown=lockdown) + service = PcapdService(lockdown=service_provider) packets_generator = service.watch(packets_count=count, process=process) if out is not None: diff --git a/pymobiledevice3/cli/power_assertion.py b/pymobiledevice3/cli/power_assertion.py index 7e449925d..48cdc7694 100644 --- a/pymobiledevice3/cli/power_assertion.py +++ b/pymobiledevice3/cli/power_assertion.py @@ -19,8 +19,8 @@ def cli(): @click.argument('name') @click.argument('timeout', type=click.INT) @click.argument('details', required=False) -def power_assertion(lockdown: LockdownClient, type, name, timeout, details): +def power_assertion(service_provider: LockdownClient, type, name, timeout, details): """ Create a power assertion (wraps IOPMAssertionCreateWithName()) """ - with PowerAssertionService(lockdown).create_power_assertion(type, name, timeout, details): + with PowerAssertionService(service_provider).create_power_assertion(type, name, timeout, details): print('> Hit Ctrl+C to exit') time.sleep(timeout) diff --git a/pymobiledevice3/cli/processes.py b/pymobiledevice3/cli/processes.py index 6f9b9954c..ed733a66e 100644 --- a/pymobiledevice3/cli/processes.py +++ b/pymobiledevice3/cli/processes.py @@ -23,16 +23,16 @@ def processes(): @processes.command('ps', cls=Command) @click.option('--color/--no-color', default=True) -def processes_ps(lockdown: LockdownClient, color): +def processes_ps(service_provider: LockdownClient, color): """ show process list """ - print_json(OsTraceService(lockdown=lockdown).get_pid_list().get('Payload'), colored=color) + print_json(OsTraceService(lockdown=service_provider).get_pid_list().get('Payload'), colored=color) @processes.command('pgrep', cls=Command) @click.argument('expression') -def processes_pgrep(lockdown: LockdownClient, expression): +def processes_pgrep(service_provider: LockdownClient, expression): """ try to match processes pid by given expression (like pgrep) """ - processes_list = OsTraceService(lockdown=lockdown).get_pid_list().get('Payload') + processes_list = OsTraceService(lockdown=service_provider).get_pid_list().get('Payload') for pid, process_info in processes_list.items(): process_name = process_info.get('ProcessName') if expression in process_name: diff --git a/pymobiledevice3/cli/profile.py b/pymobiledevice3/cli/profile.py index 9320c6ed8..0fd7e064e 100644 --- a/pymobiledevice3/cli/profile.py +++ b/pymobiledevice3/cli/profile.py @@ -22,16 +22,16 @@ def profile_group(): @profile_group.command('list', cls=Command) -def profile_list(lockdown: LockdownClient): +def profile_list(service_provider: LockdownClient): """ list installed profiles """ - print_json(MobileConfigService(lockdown=lockdown).get_profile_list()) + print_json(MobileConfigService(lockdown=service_provider).get_profile_list()) @profile_group.command('install', cls=Command) @click.argument('profiles', nargs=-1, type=click.File('rb')) -def profile_install(lockdown: LockdownClient, profiles): +def profile_install(service_provider: LockdownClient, profiles): """ install given profiles """ - service = MobileConfigService(lockdown=lockdown) + service = MobileConfigService(lockdown=service_provider) for profile in profiles: logger.info(f'installing {profile.name}') service.install_profile(profile.read()) @@ -43,9 +43,9 @@ def profile_install(lockdown: LockdownClient, profiles): @click.option('--keystore-password', prompt=True, required=True, hide_input=True, help="The password for the PKCS#12 keystore.") @click.argument('profiles', nargs=-1, type=click.File('rb')) -def profile_install_silent(lockdown: LockdownClient, profiles, keystore, keystore_password): +def profile_install_silent(service_provider: LockdownClient, profiles, keystore, keystore_password): """ install given profiles without user interaction (requires the device to be supervised) """ - service = MobileConfigService(lockdown=lockdown) + service = MobileConfigService(lockdown=service_provider) for profile in profiles: logger.info(f'installing {profile.name}') service.install_profile_silent( @@ -54,16 +54,16 @@ def profile_install_silent(lockdown: LockdownClient, profiles, keystore, keystor @profile_group.command('cloud-configuration', cls=Command) @click.option('--color/--no-color', default=True) -def profile_cloud_configuration(lockdown: LockdownClient, color): +def profile_cloud_configuration(service_provider: LockdownClient, color): """ get cloud configuration """ - print_json(MobileConfigService(lockdown=lockdown).get_cloud_configuration(), colored=color) + print_json(MobileConfigService(lockdown=service_provider).get_cloud_configuration(), colored=color) @profile_group.command('store', cls=Command) @click.argument('profiles', nargs=-1, type=click.File('rb')) -def profile_store(lockdown: LockdownClient, profiles): +def profile_store(service_provider: LockdownClient, profiles): """ store profile """ - service = MobileConfigService(lockdown=lockdown) + service = MobileConfigService(lockdown=service_provider) for profile in profiles: logger.info(f'storing {profile.name}') service.store_profile(profile.read()) @@ -71,13 +71,13 @@ def profile_store(lockdown: LockdownClient, profiles): @profile_group.command('remove', cls=Command) @click.argument('name') -def profile_remove(lockdown: LockdownClient, name): +def profile_remove(service_provider: LockdownClient, name): """ remove profile by name """ - MobileConfigService(lockdown=lockdown).remove_profile(name) + MobileConfigService(lockdown=service_provider).remove_profile(name) @profile_group.command('set-wifi-power', cls=Command) @click.argument('state', type=click.Choice(['on', 'off']), required=False) -def profile_set_wifi_power(lockdown: LockdownClient, state): +def profile_set_wifi_power(service_provider: LockdownClient, state): """ change Wi-Fi power state """ - MobileConfigService(lockdown=lockdown).set_wifi_power_state(state == 'on') + MobileConfigService(lockdown=service_provider).set_wifi_power_state(state == 'on') diff --git a/pymobiledevice3/cli/provision.py b/pymobiledevice3/cli/provision.py index 5c5940bad..de1c48a7c 100644 --- a/pymobiledevice3/cli/provision.py +++ b/pymobiledevice3/cli/provision.py @@ -24,37 +24,37 @@ def provision(): @provision.command('install', cls=Command) @click.argument('profile', type=click.File('rb')) -def provision_install(lockdown: LockdownClient, profile): +def provision_install(service_provider: LockdownClient, profile): """ install a provision profile (.mobileprovision file) """ - MisagentService(lockdown=lockdown).install(profile) + MisagentService(lockdown=service_provider).install(profile) @provision.command('remove', cls=Command) @click.argument('profile_id') -def provision_remove(lockdown: LockdownClient, profile_id): +def provision_remove(service_provider: LockdownClient, profile_id): """ remove a provision profile """ - MisagentService(lockdown=lockdown).remove(profile_id) + MisagentService(lockdown=service_provider).remove(profile_id) @provision.command('clear', cls=Command) -def provision_clear(lockdown: LockdownClient): +def provision_clear(service_provider: LockdownClient): """ remove all provision profiles """ - for profile in MisagentService(lockdown=lockdown).copy_all(): - MisagentService(lockdown=lockdown).remove(profile.plist['UUID']) + for profile in MisagentService(lockdown=service_provider).copy_all(): + MisagentService(lockdown=service_provider).remove(profile.plist['UUID']) @provision.command('list', cls=Command) @click.option('--color/--no-color', default=True) -def provision_list(lockdown: LockdownClient, color): +def provision_list(service_provider: LockdownClient, color): """ list installed provision profiles """ - print_json([p.plist for p in MisagentService(lockdown=lockdown).copy_all()], colored=color) + print_json([p.plist for p in MisagentService(lockdown=service_provider).copy_all()], colored=color) @provision.command('dump', cls=Command) @click.argument('out', type=click.Path(file_okay=False, dir_okay=True, exists=True)) -def provision_dump(lockdown: LockdownClient, out): +def provision_dump(service_provider: LockdownClient, out): """ dump installed provision profiles to specified location """ - for profile in MisagentService(lockdown=lockdown).copy_all(): + for profile in MisagentService(lockdown=service_provider).copy_all(): filename = f'{profile.plist["UUID"]}.mobileprovision' logger.info(f'downloading {filename}') (Path(out) / filename).write_bytes(profile.buf) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index da2e52956..b2fc8176e 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -25,26 +25,26 @@ def remote_cli(): @remote_cli.command('rsd-info', cls=Command) @click.option('--color/--no-color', default=True) -def rsd_info(lockdown: RemoteServiceDiscoveryService, color: bool): +def rsd_info(service_provider: RemoteServiceDiscoveryService, color: bool): """ show info extracted from RSD peer """ - print_json(lockdown.peer_info, colored=color) + print_json(service_provider.peer_info, colored=color) @remote_cli.command('create-listener', cls=Command) @click.option('-p', '--protocol', type=click.Choice(['quic', 'udp'])) @click.option('--color/--no-color', default=True) -def create_listener(lockdown: RemoteServiceDiscoveryService, protocol: str, color: bool): +def create_listener(service_provider: RemoteServiceDiscoveryService, protocol: str, color: bool): """ start a remote listener """ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - with create_core_device_tunnel_service(lockdown, autopair=True) as service: + with create_core_device_tunnel_service(service_provider, autopair=True) as service: print_json(service.create_listener(private_key, protocol=protocol), colored=color) @remote_cli.command('start-quic-tunnel', cls=Command) @click.option('--color/--no-color', default=True) -def start_quic_tunnel(lockdown: RemoteServiceDiscoveryService, color: bool): +def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, color: bool): """ start quic tunnel """ logger.critical('This is a WIP command. Will only print the required parameters for the quic connection') private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - with create_core_device_tunnel_service(lockdown, autopair=True) as service: + with create_core_device_tunnel_service(service_provider, autopair=True) as service: print_json(asyncio.run(service.start_quic_tunnel(private_key)), colored=color) diff --git a/pymobiledevice3/cli/springboard.py b/pymobiledevice3/cli/springboard.py index 55528a7ca..f77685863 100644 --- a/pymobiledevice3/cli/springboard.py +++ b/pymobiledevice3/cli/springboard.py @@ -30,15 +30,15 @@ def state(): @state.command('get', cls=Command) @click.option('--color/--no-color', default=True) -def state_get(lockdown: LockdownClient, color): +def state_get(service_provider: LockdownClient, color): """ get icon state """ - print_json(SpringBoardServicesService(lockdown=lockdown).get_icon_state(), colored=color) + print_json(SpringBoardServicesService(lockdown=service_provider).get_icon_state(), colored=color) @springboard.command('shell', cls=Command) -def springboard_shell(lockdown: LockdownClient): +def springboard_shell(service_provider: LockdownClient): """ open a shell to communicate with SpringBoardServicesService """ - service = SpringBoardServicesService(lockdown=lockdown) + service = SpringBoardServicesService(lockdown=service_provider) IPython.embed( header=SHELL_USAGE, user_ns={ @@ -49,9 +49,9 @@ def springboard_shell(lockdown: LockdownClient): @springboard.command('icon', cls=Command) @click.argument('bundle_id') @click.argument('out', type=click.File('wb')) -def springboard_icon(lockdown: LockdownClient, bundle_id, out): +def springboard_icon(service_provider: LockdownClient, bundle_id, out): """ get application's icon """ - out.write(SpringBoardServicesService(lockdown=lockdown).get_icon_pngdata(bundle_id)) + out.write(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id)) @springboard.command('orientation', cls=Command) @@ -62,6 +62,6 @@ def springboard_orientation(lockdown: LockdownClient): @springboard.command('wallpaper', cls=Command) @click.argument('out', type=click.File('wb')) -def springboard_wallpaper(lockdown: LockdownClient, out): +def springboard_wallpaper(service_provider: LockdownClient, out): """ get wallpapaer """ - out.write(SpringBoardServicesService(lockdown=lockdown).get_wallpaper_pngdata()) + out.write(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata()) diff --git a/pymobiledevice3/cli/syslog.py b/pymobiledevice3/cli/syslog.py index 30a78d36e..378d91d72 100644 --- a/pymobiledevice3/cli/syslog.py +++ b/pymobiledevice3/cli/syslog.py @@ -27,9 +27,9 @@ def syslog(): @syslog.command('live-old', cls=Command) -def syslog_live_old(lockdown: LockdownClient): +def syslog_live_old(service_provider: LockdownClient): """ view live syslog lines in raw bytes form from old relay """ - for line in SyslogService(lockdown=lockdown).watch(): + for line in SyslogService(lockdown=service_provider).watch(): print(line) @@ -90,7 +90,7 @@ def format_line(color, pid, syslog_entry, include_label): @click.option('include_label', '--label', is_flag=True, help='should include label') @click.option('-e', '--regex', multiple=True, help='filter only lines matching given regex') @click.option('-ei', '--insensitive-regex', multiple=True, help='filter only lines matching given regex (insensitive)') -def syslog_live(lockdown: LockdownClient, out, color, pid, process_name, match, match_insensitive, include_label, regex, +def syslog_live(service_provider: LockdownClient, out, color, pid, process_name, match, match_insensitive, include_label, regex, insensitive_regex): """ view live syslog lines """ @@ -102,7 +102,7 @@ def replace(m): return line.replace(m.group(1), colored(m.group(1), attrs=['bold', 'underline'])) return None - for syslog_entry in OsTraceService(lockdown=lockdown).syslog(pid=pid): + for syslog_entry in OsTraceService(lockdown=service_provider).syslog(pid=pid): if process_name: if posixpath.basename(syslog_entry.filename) != process_name: continue @@ -157,7 +157,7 @@ def replace(m): @click.option('--size-limit', type=click.INT) @click.option('--age-limit', type=click.INT) @click.option('--start-time', type=click.INT) -def syslog_collect(lockdown: LockdownClient, out, size_limit, age_limit, start_time): +def syslog_collect(service_provider: LockdownClient, out, size_limit, age_limit, start_time): """ Collect the system logs into a .logarchive that can be viewed later with tools such as log or Console. If the filename doesn't exist, system_logs.logarchive will be created in the given directory. @@ -173,4 +173,4 @@ def syslog_collect(lockdown: LockdownClient, out, size_limit, age_limit, start_t logger.warning('given out path doesn\'t end with a .logarchive - consider renaming to be able to view ' 'the file with the likes of the Console.app and the `log show` utilities') - OsTraceService(lockdown=lockdown).collect(out, size_limit=size_limit, age_limit=age_limit, start_time=start_time) + OsTraceService(lockdown=service_provider).collect(out, size_limit=size_limit, age_limit=age_limit, start_time=start_time) diff --git a/pymobiledevice3/cli/webinspector.py b/pymobiledevice3/cli/webinspector.py index 32334d81f..fac62e38a 100644 --- a/pymobiledevice3/cli/webinspector.py +++ b/pymobiledevice3/cli/webinspector.py @@ -75,7 +75,7 @@ def create_webinspector_and_launch_app(lockdown: LockdownClient, timeout: float, @click.option('-v', '--verbose', is_flag=True) @click.option('-t', '--timeout', default=3, show_default=True, type=float) @catch_errors -def opened_tabs(lockdown: LockdownClient, verbose, timeout): +def opened_tabs(service_provider: LockdownClient, verbose, timeout): """ Show all currently opened tabs. @@ -83,7 +83,7 @@ def opened_tabs(lockdown: LockdownClient, verbose, timeout): Opt-in: Settings -> Safari -> Advanced -> Web Inspector """ - inspector = WebinspectorService(lockdown=lockdown, loop=asyncio.get_event_loop()) + inspector = WebinspectorService(lockdown=service_provider, loop=asyncio.get_event_loop()) inspector.connect(timeout) while not inspector.connected_application: inspector.flush_input() @@ -107,7 +107,7 @@ def opened_tabs(lockdown: LockdownClient, verbose, timeout): @click.argument('url') @click.option('-t', '--timeout', default=3, show_default=True, type=float) @catch_errors -def launch(lockdown: LockdownClient, url, timeout): +def launch(service_provider: LockdownClient, url, timeout): """ Launch a specific URL in Safari. @@ -116,7 +116,7 @@ def launch(lockdown: LockdownClient, url, timeout): Settings -> Safari -> Advanced -> Web Inspector Settings -> Safari -> Advanced -> Remote Automation """ - inspector, safari = create_webinspector_and_launch_app(lockdown, timeout, SAFARI) + inspector, safari = create_webinspector_and_launch_app(service_provider, timeout, SAFARI) session = inspector.automation_session(safari) driver = WebDriver(session) print('Starting session') @@ -152,7 +152,7 @@ def launch(lockdown: LockdownClient, url, timeout): @webinspector.command(cls=Command) @click.option('-t', '--timeout', default=3, show_default=True, type=float) @catch_errors -def shell(lockdown: LockdownClient, timeout): +def shell(service_provider: LockdownClient, timeout): """ Create an IPython shell for interacting with a WebView. @@ -161,7 +161,7 @@ def shell(lockdown: LockdownClient, timeout): Settings -> Safari -> Advanced -> Web Inspector Settings -> Safari -> Advanced -> Remote Automation """ - inspector, safari = create_webinspector_and_launch_app(lockdown, timeout, SAFARI) + inspector, safari = create_webinspector_and_launch_app(service_provider, timeout, SAFARI) session = inspector.automation_session(safari) driver = WebDriver(session) try: @@ -182,7 +182,7 @@ def shell(lockdown: LockdownClient, timeout): @click.option('--automation', is_flag=True, help='Use remote automation') @click.argument('url', required=False, default='') @catch_errors -def js_shell(lockdown: LockdownClient, timeout, automation, url): +def js_shell(service_provider: LockdownClient, timeout, automation, url): """ Create a javascript shell. This interpreter runs on your local machine, but evaluates each expression on the remote @@ -197,7 +197,7 @@ def js_shell(lockdown: LockdownClient, timeout, automation, url): """ js_shell_class = AutomationJsShell if automation else InspectorJsShell - asyncio.run(run_js_shell(js_shell_class, lockdown, timeout, url)) + asyncio.run(run_js_shell(js_shell_class, service_provider, timeout, url)) udid = '' @@ -212,7 +212,7 @@ def create_app(): @webinspector.command(cls=Command) @click.option('--host', default='127.0.0.1') @click.option('--port', type=click.INT, default=9222) -def cdp(lockdown: LockdownClient, host, port): +def cdp(service_provider: LockdownClient, host, port): """ Start a CDP server for debugging WebViews. @@ -221,7 +221,7 @@ def cdp(lockdown: LockdownClient, host, port): chrome://inspect/#devices """ global udid - udid = lockdown.udid + udid = service_provider.udid uvicorn.run('pymobiledevice3.cli.webinspector:create_app', host=host, port=port, factory=True, ws_ping_timeout=None, ws='wsproto', loop='asyncio') From c48391ced2b4d7c46a5afed78692c8af3c324dcc Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 20 Jul 2023 13:52:38 +0300 Subject: [PATCH 127/234] cli_common: refactor `Command` to support `RSDCommand` --- pymobiledevice3/cli/cli_common.py | 43 ++++++++++++++++++++++-------- pymobiledevice3/cli/diagnostics.py | 3 ++- pymobiledevice3/cli/syslog.py | 6 +++-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index dda44abca..db40ae056 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -66,24 +66,23 @@ def prompt_device_list(device_list: List): raise NoDeviceSelectedError() -class Command(click.Command): +class BaseCommand(click.Command): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.params[:0] = [ - click.Option(('service_provider', '--rsd'), type=(str, int), callback=self.rsd, - help='RSD hostname and port number'), - click.Option(('service_provider', '--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 - 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 + +class LockdownCommand(BaseCommand): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.params[:0] = [ + click.Option(('service_provider', '--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'), + ] def udid(self, ctx, param: str, value: str) -> Optional[LockdownClient]: if '_PYMOBILEDEVICE3_COMPLETE' in os.environ: @@ -103,6 +102,28 @@ def udid(self, ctx, param: str, value: str) -> Optional[LockdownClient]: return prompt_device_list([create_using_usbmux(serial=device.serial) for device in devices]) +class RSDCommand(BaseCommand): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.params[:0] = [ + click.Option(('service_provider', '--rsd'), type=(str, int), callback=self.rsd, required=True, + help='RSD hostname and port number'), + ] + + 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 + + +class Command(RSDCommand, LockdownCommand): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # make the RSD optional + self.params[0].required = False + + class CommandWithoutAutopair(Command): @staticmethod def udid(ctx, param, value): diff --git a/pymobiledevice3/cli/diagnostics.py b/pymobiledevice3/cli/diagnostics.py index 7037b7ec5..0926c74d5 100644 --- a/pymobiledevice3/cli/diagnostics.py +++ b/pymobiledevice3/cli/diagnostics.py @@ -54,7 +54,8 @@ def diagnostics_info(service_provider: LockdownClient, color): @click.option('--color/--no-color', default=True) def diagnostics_ioregistry(service_provider: LockdownClient, plane, name, ioclass, color): """ get ioregistry info """ - print_json(DiagnosticsService(lockdown=service_provider).ioregistry(plane=plane, name=name, ioclass=ioclass), colored=color) + print_json(DiagnosticsService(lockdown=service_provider).ioregistry(plane=plane, name=name, ioclass=ioclass), + colored=color) @diagnostics.command('mg', cls=Command) diff --git a/pymobiledevice3/cli/syslog.py b/pymobiledevice3/cli/syslog.py index 378d91d72..7868f6192 100644 --- a/pymobiledevice3/cli/syslog.py +++ b/pymobiledevice3/cli/syslog.py @@ -90,7 +90,8 @@ def format_line(color, pid, syslog_entry, include_label): @click.option('include_label', '--label', is_flag=True, help='should include label') @click.option('-e', '--regex', multiple=True, help='filter only lines matching given regex') @click.option('-ei', '--insensitive-regex', multiple=True, help='filter only lines matching given regex (insensitive)') -def syslog_live(service_provider: LockdownClient, out, color, pid, process_name, match, match_insensitive, include_label, regex, +def syslog_live(service_provider: LockdownClient, out, color, pid, process_name, match, match_insensitive, + include_label, regex, insensitive_regex): """ view live syslog lines """ @@ -173,4 +174,5 @@ def syslog_collect(service_provider: LockdownClient, out, size_limit, age_limit, logger.warning('given out path doesn\'t end with a .logarchive - consider renaming to be able to view ' 'the file with the likes of the Console.app and the `log show` utilities') - OsTraceService(lockdown=service_provider).collect(out, size_limit=size_limit, age_limit=age_limit, start_time=start_time) + OsTraceService(lockdown=service_provider).collect(out, size_limit=size_limit, age_limit=age_limit, + start_time=start_time) From 306185c15484f7c0da03a13c6da409da54cd5737 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:45:48 +0300 Subject: [PATCH 128/234] cli: update `developer` subcommand with the new `CoreDevice` utils --- pymobiledevice3/cli/developer.py | 67 ++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 44ebd902f..7924bf98c 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -5,7 +5,6 @@ import posixpath import shlex import signal -import time from collections import namedtuple from dataclasses import asdict from datetime import datetime @@ -18,10 +17,14 @@ from termcolor import colored import pymobiledevice3 -from pymobiledevice3.cli.cli_common import BASED_INT, Command, default_json_encoder, print_json, wait_return +from pymobiledevice3.cli.cli_common import BASED_INT, Command, RSDCommand, default_json_encoder, print_json, wait_return from pymobiledevice3.exceptions import DeviceAlreadyInUseError, DvtDirListError, ExtractingStackshotError, \ UnrecognizedSelectorError from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.remote.core_device.app_service import AppServiceService +from pymobiledevice3.remote.core_device.device_info import DeviceInfoService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService from pymobiledevice3.services.accessibilityaudit import AccessibilityAudit from pymobiledevice3.services.debugserver_applist import DebugServerAppList from pymobiledevice3.services.device_arbitration import DtDeviceArbitration @@ -442,7 +445,8 @@ def stackshot(service_provider: LockdownClient, out, color): @subclass_filter @click.option('--process', default=None, help='Process ID / name to filter. Omit for all.') @click.option('--color/--no-color', default=True) -def parse_live_profile_session(service_provider: LockdownClient, count, tid, show_tid, bsc, class_filters, subclass_filters, +def parse_live_profile_session(service_provider: LockdownClient, count, tid, show_tid, bsc, class_filters, + subclass_filters, process, color): """ Print traces (syscalls, thread events, etc.) received from the device in real time. """ with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: @@ -904,3 +908,60 @@ def dvt_har(service_provider: LockdownClient): with ActivityTraceTap(dvt, enable_http_archive_logging=True) as tap: while True: tap.channel.receive_message() + + +@developer.group() +def core_device(): + """ core-device options """ + pass + + +@core_device.command('list-processes', cls=RSDCommand) +@click.option('--color/--no-color', default=True) +def core_device_list_processes(service_provider: RemoteServiceDiscoveryService, color: bool): + """ Get process list """ + with AppServiceService(service_provider) as app_service: + print_json(app_service.list_processes(), colored=color) + + +@core_device.command('uninstall', cls=RSDCommand) +@click.argument('bundle_identifier') +def core_device_uninstall_app(service_provider: RemoteServiceDiscoveryService, bundle_identifier: str): + """ Uninstall application """ + with AppServiceService(service_provider) as app_service: + app_service.uninstall_app(bundle_identifier) + + +@core_device.command('send-signal-to-process', cls=RSDCommand) +@click.argument('pid', type=click.INT) +@click.argument('signal', type=click.INT) +@click.option('--color/--no-color', default=True) +def core_device_send_signal_to_process(service_provider: RemoteServiceDiscoveryService, pid: int, signal: int, + color: bool): + """ Send signal to process """ + with AppServiceService(service_provider) as app_service: + print_json(app_service.send_signal_to_process(pid, signal), colored=color) + + +@core_device.command('get-device-info', cls=RSDCommand) +@click.option('--color/--no-color', default=True) +def core_device_get_device_info(service_provider: RemoteServiceDiscoveryService, color: bool): + """ Get device information """ + with DeviceInfoService(service_provider) as app_service: + print_json(app_service.get_device_info(), colored=color) + + +@core_device.command('get-lockstate', cls=RSDCommand) +@click.option('--color/--no-color', default=True) +def core_device_get_lockstate(service_provider: RemoteServiceDiscoveryService, color: bool): + """ Get lockstate """ + with DeviceInfoService(service_provider) as app_service: + print_json(app_service.get_lockstate(), colored=color) + + +@core_device.command('list-apps', cls=RSDCommand) +@click.option('--color/--no-color', default=True) +def core_device_list_apps(service_provider: RemoteServiceDiscoveryService, color: bool): + """ Get application list """ + with AppServiceService(service_provider) as app_service: + print_json(app_service.list_apps(), colored=color) From b864fd4d5f4a567f7360c328d843759b0f015fef Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 20 Jul 2023 13:55:30 +0300 Subject: [PATCH 129/234] cli: update `remote` cli --- pymobiledevice3/cli/remote.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index b2fc8176e..53c4224df 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -4,9 +4,10 @@ import click from cryptography.hazmat.primitives.asymmetric import rsa -from pymobiledevice3.cli.cli_common import Command, print_json +from pymobiledevice3.cli.cli_common import RSDCommand, print_json +from pymobiledevice3.remote.bonjour import get_remoted_addresses from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service -from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService logger = logging.getLogger(__name__) @@ -23,14 +24,29 @@ def remote_cli(): pass -@remote_cli.command('rsd-info', cls=Command) +@remote_cli.command('browse') +@click.option('--color/--no-color', default=True) +def browse(color: bool): + """ browse devices using bonjour """ + devices = [] + for address in get_remoted_addresses(): + with RemoteServiceDiscoveryService((address, RSD_PORT)) as rsd: + devices.append({'address': address, + 'port': RSD_PORT, + 'UniqueDeviceID': rsd.peer_info['Properties']['UniqueDeviceID'], + 'ProductType': rsd.peer_info['Properties']['ProductType'], + 'OSVersion': rsd.peer_info['Properties']['OSVersion']}) + print_json(devices, colored=color) + + +@remote_cli.command('rsd-info', cls=RSDCommand) @click.option('--color/--no-color', default=True) def rsd_info(service_provider: RemoteServiceDiscoveryService, color: bool): """ show info extracted from RSD peer """ print_json(service_provider.peer_info, colored=color) -@remote_cli.command('create-listener', cls=Command) +@remote_cli.command('create-listener', cls=RSDCommand) @click.option('-p', '--protocol', type=click.Choice(['quic', 'udp'])) @click.option('--color/--no-color', default=True) def create_listener(service_provider: RemoteServiceDiscoveryService, protocol: str, color: bool): @@ -40,7 +56,7 @@ def create_listener(service_provider: RemoteServiceDiscoveryService, protocol: s print_json(service.create_listener(private_key, protocol=protocol), colored=color) -@remote_cli.command('start-quic-tunnel', cls=Command) +@remote_cli.command('start-quic-tunnel', cls=RSDCommand) @click.option('--color/--no-color', default=True) def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, color: bool): """ start quic tunnel """ From 7ddf5c3de77fe8d8205c261680619d0b256030da Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 23 Jul 2023 17:05:28 +0300 Subject: [PATCH 130/234] python-app: improve `flake8` checks --- .github/workflows/python-app.yml | 5 +---- pymobiledevice3/services/remote_server.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 71876364f..c4d0ed63a 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -28,10 +28,7 @@ jobs: - name: Lint with flake8 run: | python -m pip install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82,F401,E741 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --max-line-length=127 - name: Verify sorted imports run: | python -m pip install isort diff --git a/pymobiledevice3/services/remote_server.py b/pymobiledevice3/services/remote_server.py index 9bf17a97b..68f0a6f63 100644 --- a/pymobiledevice3/services/remote_server.py +++ b/pymobiledevice3/services/remote_server.py @@ -231,7 +231,7 @@ class RemoteServer(LockdownService): } } ``` - """ + """ # noqa: E501 BROADCAST_CHANNEL = 0 INSTRUMENTS_MESSAGE_TYPE = 2 EXPECTS_REPLY_MASK = 0x1000 From e169af19865add6a9789b26561a80c214cf2afb9 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 23 Jul 2023 19:07:33 +0300 Subject: [PATCH 131/234] xpc_message: add more primitives serialization --- pymobiledevice3/remote/xpc_message.py | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/remote/xpc_message.py b/pymobiledevice3/remote/xpc_message.py index 7d42eb1dc..a2b9b05a7 100644 --- a/pymobiledevice3/remote/xpc_message.py +++ b/pymobiledevice3/remote/xpc_message.py @@ -6,7 +6,7 @@ from construct import Aligned, Array, Bytes, Const, CString, Default, Double, Enum, ExprAdapter, FlagsEnum, \ GreedyBytes, Hex, If, Int32ul, Int64sl, Int64ul, LazyBound from construct import Optional as ConstructOptional -from construct import Prefixed, Probe, Struct, Switch, this +from construct import Pass, Prefixed, Probe, Struct, Switch, this XpcMessageType = Enum(Hex(Int32ul), NULL=0x00001000, @@ -47,7 +47,7 @@ INIT_HANDSHAKE=0x00400000, ) AlignedString = Aligned(4, CString('utf8')) -XpcNull = None +XpcNull = Pass XpcBool = Int32ul XpcInt64 = Int64sl XpcUInt64 = Int64ul @@ -173,6 +173,14 @@ def _decode_xpc_file_transfer(xpc_object) -> FileTransferType: return FileTransferType(transfer_size=_decode_xpc_dictionary(xpc_object.data.data)['s']) +def _decode_xpc_double(xpc_object) -> float: + return xpc_object.data + + +def _decode_xpc_null(xpc_object) -> None: + return None + + def decode_xpc_object(xpc_object) -> Any: decoders = { XpcMessageType.DICTIONARY: _decode_xpc_dictionary, @@ -185,6 +193,8 @@ def decode_xpc_object(xpc_object) -> Any: XpcMessageType.DATA: _decode_xpc_data, XpcMessageType.DATE: _decode_xpc_date, XpcMessageType.FILE_TRANSFER: _decode_xpc_file_transfer, + XpcMessageType.DOUBLE: _decode_xpc_double, + XpcMessageType.NULL: _decode_xpc_null, } decoder = decoders.get(xpc_object.type) if decoder is None: @@ -248,6 +258,20 @@ def _build_xpc_double(payload: float) -> Mapping: } +def _build_xpc_uuid(payload: uuid.UUID) -> Mapping: + return { + 'type': XpcMessageType.UUID, + 'data': payload.bytes, + } + + +def _build_xpc_null(payload: None) -> Mapping: + return { + 'type': XpcMessageType.NULL, + 'data': None, + } + + def _build_xpc_uint64(payload: XpcUInt64Type) -> Mapping: return { 'type': XpcMessageType.UINT64, @@ -263,6 +287,8 @@ def _build_xpc_int64(payload: XpcInt64Type) -> Mapping: def _build_xpc_object(payload: Any) -> Mapping: + if payload is None: + return _build_xpc_null(payload) payload_builders = { list: _build_xpc_array, dict: _build_xpc_dictionary, @@ -271,6 +297,7 @@ def _build_xpc_object(payload: Any) -> Mapping: bytes: _build_xpc_data, bytearray: _build_xpc_data, float: _build_xpc_double, + uuid.UUID: _build_xpc_uuid, 'XpcUInt64Type': _build_xpc_uint64, 'XpcInt64Type': _build_xpc_int64, } From 5c713b499a0a4240b41fd660555c2cd2998adcea Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 23 Jul 2023 19:08:07 +0300 Subject: [PATCH 132/234] app_service: fix `user` value for `spawnexecutable` --- pymobiledevice3/remote/core_device/app_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pymobiledevice3/remote/core_device/app_service.py b/pymobiledevice3/remote/core_device/app_service.py index c8f9ef3f8..2d04c211f 100644 --- a/pymobiledevice3/remote/core_device/app_service.py +++ b/pymobiledevice3/remote/core_device/app_service.py @@ -61,8 +61,7 @@ def spawn_executable(self, executable: str, arguments: List[str]) -> Mapping: 'standardIOUsesPseudoterminals': True, 'startStopped': False, 'user': { - 'shortName': 'short-name', - + 'active': True, }, 'platformSpecificOptions': plistlib.dumps({}), }, From 7795aefe8ee186013f0283650c77f2d16a49feab Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:46:33 +0300 Subject: [PATCH 133/234] move doc and test utils into `misc` subdirectory --- DTServices-14.2.txt => misc/DTServices-14.2.txt | 0 DTServices-14.5.txt => misc/DTServices-14.5.txt | 0 extract_plist_from_pcaps.py => misc/extract_plist_from_pcaps.py | 0 pymobiledevice3/remote/sniffer.py => misc/remotexpc_sniffer.py | 2 +- usbmux_sniff.sh => misc/usbmux_sniff.sh | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename DTServices-14.2.txt => misc/DTServices-14.2.txt (100%) rename DTServices-14.5.txt => misc/DTServices-14.5.txt (100%) rename extract_plist_from_pcaps.py => misc/extract_plist_from_pcaps.py (100%) rename pymobiledevice3/remote/sniffer.py => misc/remotexpc_sniffer.py (99%) rename usbmux_sniff.sh => misc/usbmux_sniff.sh (100%) diff --git a/DTServices-14.2.txt b/misc/DTServices-14.2.txt similarity index 100% rename from DTServices-14.2.txt rename to misc/DTServices-14.2.txt diff --git a/DTServices-14.5.txt b/misc/DTServices-14.5.txt similarity index 100% rename from DTServices-14.5.txt rename to misc/DTServices-14.5.txt diff --git a/extract_plist_from_pcaps.py b/misc/extract_plist_from_pcaps.py similarity index 100% rename from extract_plist_from_pcaps.py rename to misc/extract_plist_from_pcaps.py diff --git a/pymobiledevice3/remote/sniffer.py b/misc/remotexpc_sniffer.py similarity index 99% rename from pymobiledevice3/remote/sniffer.py rename to misc/remotexpc_sniffer.py index 483791e63..fc03a3877 100644 --- a/pymobiledevice3/remote/sniffer.py +++ b/misc/remotexpc_sniffer.py @@ -7,13 +7,13 @@ from construct import ConstError, StreamError from hexdump import hexdump from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame -from remotexpc import HTTP2_MAGIC from scapy.layers.inet import IP, TCP from scapy.layers.inet6 import IPv6 from scapy.packet import Packet from scapy.sendrecv import sniff from pymobiledevice3.remote.core_device_tunnel_service import PairingDataComponentTLVBuf +from pymobiledevice3.remote.remotexpc import HTTP2_MAGIC from pymobiledevice3.remote.xpc_message import XpcWrapper, decode_xpc_object logger = logging.getLogger() diff --git a/usbmux_sniff.sh b/misc/usbmux_sniff.sh similarity index 100% rename from usbmux_sniff.sh rename to misc/usbmux_sniff.sh From 1f7439f3aa33ef032e001ef459e3c693bcef22df Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 24 Jul 2023 08:14:22 +0300 Subject: [PATCH 134/234] README: add iOS 17.0 disclaimer --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 371d90ad3..a0ff6bd9f 100644 --- a/README.md +++ b/README.md @@ -179,18 +179,18 @@ There is A LOT you may do on the device using `pymobiledevice3`. This is just a * `pymobiledevice3 webinspector opened-tabs` * The following will require also the Remote Automation feature to be turned on: * Get interactive JavaScript shell on new remote automation tab: - * `pymobiledevice3 webinspector js_shell --automation` + * `pymobiledevice3 webinspector js_shell --automation` * Launch an automation session to view a given URL: * `pymobiledevice3 webinspector launch URL` * Get a a selenium-like shell: * `pymobiledevice3 webinspector shell` -* Mount DeveloperDiskImage: - * `pymobiledevice3 mounter mount` +* Mount DeveloperDiskImage (On iOS>=17.0, each command will require an additional `--rsd` option): + * `pymobiledevice3 mounter auto-mount` * The following will assume the DeveloperDiskImage is already mounted: * Simulate an `x y` location: * `pymobiledevice3 developer simulate-location set x y` * Taking a screenshot from the device: - * `pymobiledevice3 developer screenshot /path/to/screen.png` + * `pymobiledevice3 developer dvt screenshot /path/to/screen.png` * View detailed process list (including ppid, uid, guid, sandboxed, etc...): * `pymobiledevice3 developer dvt sysmon process single` * Sniffing oslog: From 8ed40bb90ad7cc32c23e7820399e275f28e20508 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 19 Jul 2023 14:46:46 +0300 Subject: [PATCH 135/234] misc: add `RemoteXPC.md` --- misc/RemoteXPC.md | 831 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 831 insertions(+) create mode 100644 misc/RemoteXPC.md diff --git a/misc/RemoteXPC.md b/misc/RemoteXPC.md new file mode 100644 index 000000000..5bf35cd3b --- /dev/null +++ b/misc/RemoteXPC.md @@ -0,0 +1,831 @@ +- [RemoteXPC](#remotexpc) + * [Overview](#overview) + * [Previous research](#previous-research) + * [USB Ethernet](#usb-ethernet) + * [Process: `remoted`](#process-remoted) + * [Pairing](#pairing) + * [Trusted tunnel](#trusted-tunnel) + + [Reusing the macOS trusted tunnel](#reusing-the-macos-trusted-tunnel) + * [Accessing services over the trusted tunnel](#accessing-services-over-the-trusted-tunnel) + + [Lockdown services](#lockdown-services) + + [RemoteXPC services](#remotexpc-services) + - [CoreDevice services](#coredevice-services) + * [Using `pymobiledevice3` as a client](#using-pymobiledevice3-as-a-client) + + [Handshake & Pairing](#handshake--pairing) + + [Accessing services over RemoteXPC](#accessing-services-over-remotexpc) + +# RemoteXPC + +## Overview + +Starting at iOS 17.0, Apple refactored a lot in the way iOS devices communicate with the macOS. Up until iOS 16, The +communication was TCP based (using the help of `usbmuxd` for USB devices) with TLS (for making sure only trusted peers +are able to connect). You can read more about +the old protocol in this article: + +https://jon-gabilondo-angulo-7635.medium.com/understanding-usbmux-and-the-ios-lockdown-service-7f2a1dfd07ae + +The new protocol stack relies on [QUIC](https://en.wikipedia.org/wiki/QUIC)+RemoteXPC which should reduce much of the +communication overhead in general - allowing faster and more stable connections, especially over WiFi. + +## Previous research + +RemoteXPC was introduced for macOS much earlier. You can read more about it here: + +https://duo.com/labs/research/apple-t2-xpc + +However, our protocol stack is a bit different. + +## USB Ethernet + +Starting in iOS 17, whenever you connect an iPhone to your macOS, it creates a new network device (with an IPv6 address +😱) using [Ethernet over USB](https://en.wikipedia.org/wiki/Ethernet_over_USB) - Meaning, the device is always on +your LAN and you can communicate with it using Ethernet protocols. + +## Process: `remoted` + +Each Apple device runs a daemon named `remoted`. This daemon allows processes running on the same host to register XPC +services they wish to export to other clients over the network (hence the RemoteXPC name). + +Other processes can ask (over XPC) to `browse` for newly connected devices. This browse occurs +using [bonjour](https://en.wikipedia.org/wiki/Bonjour_(software)). + +Once a device is found, `remoted` establishes a RemoteXPC connection (XPC dictionaries serialized +over [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2)) to the RSD (RemoteServiceDiscovery) port (hard-coded `58783`) to +get a list of exported services: + +
+Show RSD handshake response + +```json +{ + "MessageType": "Handshake", + "MessagingProtocolVersion": 3, + "Properties": { + "AppleInternal": false, + "BoardId": 14, + "BootSessionUUID": "a4ba4745-5925-4e45-93ab-46ec91880c91", + "BuildVersion": "21A5277j", + "CPUArchitecture": "arm64e", + "CertificateProductionStatus": true, + "CertificateSecurityMode": true, + "ChipID": 33056, + "DeviceClass": "iPhone", + "DeviceColor": "1", + "DeviceEnclosureColor": "1", + "DeviceSupportsLockdown": true, + "EffectiveProductionStatusAp": true, + "EffectiveProductionStatusSEP": true, + "EffectiveSecurityModeAp": true, + "EffectiveSecurityModeSEP": true, + "EthernetMacAddress": "aa:bb:cc:dd:ee:ff", + "HWModel": "D74AP", + "HardwarePlatform": "t8120", + "HasSEP": true, + "HumanReadableProductVersionString": "17.0", + "Image4CryptoHashMethod": "sha2-384", + "Image4Supported": true, + "IsUIBuild": true, + "IsVirtualDevice": false, + "MobileDeviceMinimumVersion": "1600", + "ModelNumber": "MQ9U3", + "OSInstallEnvironment": false, + "OSVersion": "17.0", + "ProductName": "iPhone OS", + "ProductType": "iPhone15,3", + "RegionCode": "HX", + "RegionInfo": "HX/A", + "ReleaseType": "Beta", + "RemoteXPCVersionFlags": 72057594037927942, + "RestoreLongVersion": "21.1.277.5.10,0", + "SecurityDomain": 1, + "SensitivePropertiesVisible": true, + "SerialNumber": 1111111, + "SigningFuse": true, + "StoreDemoMode": false, + "SupplementalBuildVersion": "21A5277j", + "ThinningProductType": "iPhone15,3", + "UniqueChipID": 111111, + "UniqueDeviceID": "222222222" + }, + "Services": { + "com.apple.fusion.remote.service": { + "Entitlement": "com.apple.fusion.remote.service", + "Port": "52286", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.gputools.remote.agent": { + "Entitlement": "com.apple.private.gputoolstransportd", + "Port": "52292", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.internal.dt.coredevice.untrusted.tunnelservice": { + "Entitlement": "com.apple.dt.coredevice.tunnelservice.client", + "Port": "52291", + "Properties": { + "ServiceVersion": 2, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.insecure_notification_proxy.remote": { + "Entitlement": "com.apple.mobile.insecure_notification_proxy.remote", + "Port": "52289", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.insecure_notification_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.untrusted", + "Port": "52287" + }, + "com.apple.mobile.lockdown.remote.untrusted": { + "Entitlement": "com.apple.mobile.lockdown.remote.untrusted", + "Port": "52288", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.osanalytics.logTransfer": { + "Entitlement": "com.apple.ReportCrash.antenna-access", + "Port": "52290", + "Properties": { + "UsesRemoteXPC": true + } + } + }, + "UUID": "1d701c76-cf8e-45c7-a6c9-d794ee85411c" +} +``` + +
+ +As you can see, we get quite some info: + +- The device general information +- Each service may report the following metadata: + - `UsesRemoteXPC`: Whether the communication is done over RemoteXPC or not. + - `Entitlement`: From my understanding, this just regards the entitlement needed by the + connecting on-device client. + - `ServiceVersion`: Probably refers to some protocol changes being done to help backward compatibility of other + clients. + +Each of this services can be accessed from any untrusted peer. + +## Pairing + +One of the clients asking `remoted` for `browse` is `remotepairingd` which is in charge of.. well.. pairing. +It does so via the `com.apple.internal.dt.coredevice.untrusted.tunnelservice` service. + +The pairing is done in a state machine as follows: + +- Wait user consent (The "Trust / Don't Trust" dialog) +- Key exchange ([SRP](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol), with the dummy password: `000000`) +- Request to save pair record on remote device + +And... that's it! The client can now use the saved pair record to request a **trusted tunnel**. + +## Trusted tunnel + +Over the now paired connection to `com.apple.internal.dt.coredevice.untrusted.tunnelservice` the +client (`remotepairingd`) can now request to establish a trusted tunnel. This tunnel acts a VPN to the device for +trusted connections. + +The client then generates its own keypair and send the following request: + +```json +{ + "request": { + "_0": { + "createListener": { + "key": "CLIENT-PUBLIC-KEY", + "transportProtocolType": "quic" + } + } + } +} +``` + +The `transportProtocolType` specifies which transport protocol we would like to use for our VPN connection. The two +options are either `quic` which includes TLVv1.3 authentication - or a TLS over `udp` using a PSK. + +Once the request has been made, the client then receives a response with the created QUIC server public key and port +number. It then connects and receives details for creating a local TUN device that will tunnel all the trusted traffic. + +This response looks as follows: + +```json +{ + "clientParameters": { + "address": "fd58:8c92:8961::2", + "mtu": 1280, + "netmask": "ffff:ffff:ffff:ffff::" + }, + "serverAddress": "fd58:8c92:8961::1", + "serverRSDPort": 56307, + "type": "serverHandshakeResponse" +} +``` + +The `clientParameters` are used to configure a TUN device on the local machine, while the other "server" related info is +for the new **trusted RSD connection**. That's right, we are going to use this trusted to (again) use RSD, and connect +to device XPC services - but this time as a fully trusted client. + +The client now establishes another RSD connection to the specified `serverAddress` and `serverRSDPort` (which are now +done over the created TUN device, meaning they are going through a TLS encryption) and can now access new and wide range +of services. + +### Reusing the macOS trusted tunnel + +`remotepairingd` is generous enough to share this connection information into the host syslog. We can sniff +and deduct the VPN parameters by viewing the syslog (you can `sudo pkill -9 remoted` to force a reconnection): + +```shell +log stream --debug --info --predicate 'eventMessage LIKE "*Tunnel established*" OR eventMessage LIKE "*for server port*"' +``` + +The output should be something similar to: + +``` +Timestamp Thread Type Activity PID TTL +2023-07-19 08:22:51.916784+0300 0x3058 Info 0x0 599 0 remotepairingd: (RemotePairing) [com.apple.dt.remotepairing:networktunnelmanager] tunnel-1: Tunnel established for interface: utun3, local fd41:8efc:c0f8::2 -> fd41:8efc:c0f8::1 +2023-07-19 08:22:51.917310+0300 0x559c Info 0x0 599 0 remotepairingd: [com.apple.dt.remotepairing:remotepairingd] device-0: Tunnel established - interface: utun3, local fd41:8efc:c0f8::2-> remote fd41:8efc:c0f8::1 +2023-07-19 08:22:51.917414+0300 0x559c Info 0x0 599 0 remotepairingd: [com.apple.dt.remotepairing:remotepairingd] device-0: Creating RSD backend client device for server port 60364 +``` + +## Accessing services over the trusted tunnel + +The client now has a much wider list of services he is able to connect to: + +
+Show RSD handshake response + +```json +{ + "MessageType": "Handshake", + "MessagingProtocolVersion": 3, + "Properties": { + "AppleInternal": false, + "BoardId": 14, + "BootSessionUUID": "a4ba4745-5925-4e45-93ab-46ec91880c91", + "BuildVersion": "21A5277j", + "CPUArchitecture": "arm64e", + "CertificateProductionStatus": true, + "CertificateSecurityMode": true, + "ChipID": 33056, + "DeviceClass": "iPhone", + "DeviceColor": "1", + "DeviceEnclosureColor": "1", + "DeviceSupportsLockdown": true, + "EffectiveProductionStatusAp": true, + "EffectiveProductionStatusSEP": true, + "EffectiveSecurityModeAp": true, + "EffectiveSecurityModeSEP": true, + "EthernetMacAddress": "aa:bb:cc:dd:ee:ff", + "HWModel": "D74AP", + "HardwarePlatform": "t8120", + "HasSEP": true, + "HumanReadableProductVersionString": "17.0", + "Image4CryptoHashMethod": "sha2-384", + "Image4Supported": true, + "IsUIBuild": true, + "IsVirtualDevice": false, + "MobileDeviceMinimumVersion": "1600", + "ModelNumber": "MQ9U3", + "OSInstallEnvironment": false, + "OSVersion": "17.0", + "ProductName": "iPhone OS", + "ProductType": "iPhone15,3", + "RegionCode": "HX", + "RegionInfo": "HX/A", + "ReleaseType": "Beta", + "RemoteXPCVersionFlags": 72057594037927942, + "RestoreLongVersion": "21.1.277.5.10,0", + "SecurityDomain": 1, + "SensitivePropertiesVisible": true, + "SerialNumber": 1111111, + "SigningFuse": true, + "StoreDemoMode": false, + "SupplementalBuildVersion": "21A5277j", + "ThinningProductType": "iPhone15,3", + "UniqueChipID": 111111, + "UniqueDeviceID": "222222222" + }, + "Services": { + "com.apple.GPUTools.MobileService.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55307" + }, + "com.apple.PurpleReverseProxy.Conn.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55305" + }, + "com.apple.PurpleReverseProxy.Ctrl.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55291" + }, + "com.apple.RestoreRemoteServices.restoreserviced": { + "Entitlement": "com.apple.private.RestoreRemoteServices.restoreservice.remote", + "Port": "55298", + "Properties": { + "ServiceVersion": 2, + "UsesRemoteXPC": true + } + }, + "com.apple.accessibility.axAuditDaemon.remoteserver.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55269" + }, + "com.apple.afc.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55255" + }, + "com.apple.amfi.lockdown.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55261" + }, + "com.apple.atc.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55268" + }, + "com.apple.atc2.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55309" + }, + "com.apple.backgroundassets.lockdownservice.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55310" + }, + "com.apple.bluetooth.BTPacketLogger.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55266" + }, + "com.apple.carkit.remote-iap.service": { + "Entitlement": "AppleInternal", + "Port": "55296", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.carkit.service.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55312" + }, + "com.apple.commcenter.mobile-helper-cbupdateservice.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55273" + }, + "com.apple.companion_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55285" + }, + "com.apple.corecaptured.remoteservice": { + "Entitlement": "com.apple.corecaptured.remoteservice-access", + "Port": "55302", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.appservice": { + "Entitlement": "com.apple.private.CoreDevice.canInstallCustomerContent", + "Port": "55278", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.launchapplication", + "com.apple.coredevice.feature.spawnexecutable", + "com.apple.coredevice.feature.monitorprocesstermination", + "com.apple.coredevice.feature.installapp", + "com.apple.coredevice.feature.uninstallapp", + "com.apple.coredevice.feature.listroots", + "com.apple.coredevice.feature.installroot", + "com.apple.coredevice.feature.uninstallroot", + "com.apple.coredevice.feature.sendsignaltoprocess", + "com.apple.coredevice.feature.sendmemorywarningtoprocess", + "com.apple.coredevice.feature.listprocesses", + "com.apple.coredevice.feature.rebootdevice", + "com.apple.coredevice.feature.listapps", + "com.apple.coredevice.feature.fetchappicons" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.deviceinfo": { + "Entitlement": "com.apple.private.CoreDevice.canRetrieveDeviceInfo", + "Port": "55259", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.getdeviceinfo", + "com.apple.coredevice.feature.querymobilegestalt", + "com.apple.coredevice.feature.getlockstate" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.diagnosticsservice": { + "Entitlement": "com.apple.private.CoreDevice.canObtainDiagnostics", + "Port": "55274", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.capturesysdiagnose" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.fileservice.control": { + "Entitlement": "com.apple.private.CoreDevice.canTransferFilesToDevice", + "Port": "55284", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.transferFiles" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.fileservice.data": { + "Entitlement": "com.apple.private.CoreDevice.canTransferFilesToDevice", + "Port": "55275", + "Properties": { + "Features": [], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.openstdiosocket": { + "Entitlement": "com.apple.private.CoreDevice.canInstallCustomerContent", + "Port": "55301", + "Properties": { + "Features": [], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.crashreportcopymobile.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55304" + }, + "com.apple.crashreportmover.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55289" + }, + "com.apple.dt.ViewHierarchyAgent.remote": { + "Entitlement": "com.apple.private.dt.ViewHierarchyAgent.client", + "Port": "55277", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.dt.remoteFetchSymbols": { + "Entitlement": "com.apple.private.dt.remoteFetchSymbols.client", + "Port": "55263", + "Properties": { + "Features": [ + "com.apple.dt.remoteFetchSymbols.dyldSharedCacheFiles" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.dt.remotepairingdeviced.lockdown.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55293" + }, + "com.apple.dt.testmanagerd.remote": { + "Entitlement": "com.apple.private.dt.testmanagerd.client", + "Port": "55299", + "Properties": { + "UsesRemoteXPC": false + } + }, + "com.apple.dt.testmanagerd.remote.automation": { + "Entitlement": "AppleInternal", + "Port": "55260", + "Properties": { + "UsesRemoteXPC": false + } + }, + "com.apple.fusion.remote.service": { + "Entitlement": "com.apple.fusion.remote.service", + "Port": "55311", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.gputools.remote.agent": { + "Entitlement": "com.apple.private.gputoolstransportd", + "Port": "55303", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.idamd.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55264" + }, + "com.apple.instruments.dtservicehub": { + "Entitlement": "com.apple.private.dt.instruments.dtservicehub.client", + "Port": "55270", + "Properties": { + "Features": [ + "com.apple.dt.profile" + ], + "version": 1 + } + }, + "com.apple.internal.devicecompute.CoreDeviceProxy": { + "Entitlement": "AppleInternal", + "Port": "55271", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": false + } + }, + "com.apple.internal.dt.coredevice.untrusted.tunnelservice": { + "Entitlement": "com.apple.dt.coredevice.tunnelservice.client", + "Port": "55267", + "Properties": { + "ServiceVersion": 2, + "UsesRemoteXPC": true + } + }, + "com.apple.internal.dt.remote.debugproxy": { + "Entitlement": "com.apple.private.CoreDevice.canDebugApplicationsOnDevice", + "Port": "55249", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.debugserverproxy" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.iosdiagnostics.relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55287" + }, + "com.apple.misagent.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55257" + }, + "com.apple.mobile.MCInstall.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55313" + }, + "com.apple.mobile.assertion_agent.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55314" + }, + "com.apple.mobile.diagnostics_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55253" + }, + "com.apple.mobile.file_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55300" + }, + "com.apple.mobile.heartbeat.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55272" + }, + "com.apple.mobile.house_arrest.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55265" + }, + "com.apple.mobile.insecure_notification_proxy.remote": { + "Entitlement": "com.apple.mobile.insecure_notification_proxy.remote", + "Port": "55315", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.insecure_notification_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.untrusted", + "Port": "55308" + }, + "com.apple.mobile.installation_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55276" + }, + "com.apple.mobile.lockdown.remote.trusted": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55294", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.lockdown.remote.untrusted": { + "Entitlement": "com.apple.mobile.lockdown.remote.untrusted", + "Port": "55251", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.mobile_image_mounter.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55252" + }, + "com.apple.mobile.notification_proxy.remote": { + "Entitlement": "com.apple.mobile.notification_proxy.remote", + "Port": "55292", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.notification_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55286" + }, + "com.apple.mobile.storage_mounter_proxy.bridge": { + "Entitlement": "com.apple.private.mobile_storage.remote.allowedSPI", + "Port": "55279", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobileactivationd.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55295" + }, + "com.apple.mobilebackup2.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55281" + }, + "com.apple.mobilesync.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55282" + }, + "com.apple.os_trace_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55254" + }, + "com.apple.osanalytics.logTransfer": { + "Entitlement": "com.apple.ReportCrash.antenna-access", + "Port": "55256", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.pcapd.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55290" + }, + "com.apple.preboardservice.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55258" + }, + "com.apple.preboardservice_v2.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55288" + }, + "com.apple.remote.installcoordination_proxy": { + "Entitlement": "com.apple.private.InstallCoordinationRemote", + "Port": "55297", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.springboardservices.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55250" + }, + "com.apple.streaming_zip_conduit.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55280" + }, + "com.apple.sysdiagnose.remote": { + "Entitlement": "com.apple.private.sysdiagnose.remote", + "Port": "55306", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.syslog_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55283" + }, + "com.apple.webinspector.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "55262" + } + }, + "UUID": "289ff0d8-cbbb-4f46-867e-48a68f3b65f8" +} +``` + +
+ +Now let's divide them into two main groups: + +- Lockdown services +- RemoteXPC services + +### Lockdown services + +All the services that used to be accessible via `lockdownd`, are now accessible via `remoted` "directly". All the +services that require the `com.apple.mobile.lockdown.remote.trusted` entitlement will actually be spawned +via `lockdownd`, but in a transparent manner to us. + +We need to first send the following message: + +```json +{ + "Label": "userAgent", + "ProtocolVersion": "2", + "Request": "RSDCheckin", + "EscrowBag": "if any..." +} +``` + +This causes `remoted` to connect to `lockdownd` and request to start the service we want to talk to - Allowing a very +nice abstract way to keep communicating with the old device the same way we used to. + +### RemoteXPC services + +The RemoteXPC services will declare the `UsesRemoteXPC` property. We communicate with them the same was as with the RSD +service. + +#### CoreDevice services + +Some of the RemoteXPC services are CoreDevice services. We can distinguish them by having the `Features` key, telling us +of all the available methods these services support. + +The format of each XPC dictionary sent as a request is as follows: + +```python +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': feature_identifier, + 'CoreDevice.input': parameters, + 'CoreDevice.invocationIdentifier': '14A17AB8-0576-4E73-94C6-C0282A4F66E3'} +``` + +The response is just what the invoked function returned. + +## Using `pymobiledevice3` as a client + +### Handshake & Pairing + +`pymobiledevice3` also includes its own implementation of connecting to `remoted` and performing an RSD handshake. +You can try it yourself: + +```shell +pymobiledevice3 remote rsd-info --rsd HOST PORT +``` + +However, it seems trying to connect from the same host will resolve in a "fight" between `remoted` +and `pymobiledevice3`. The following message will appear on device syslog: + +``` +2023-07-23 22:11:18.608538 remoted{remoted}[50] : ncmhost-3> Canceling existing connection to replace it +``` + +This will trigger a `close()` on `remoted`'s FD, so it'll try to re-connect - which will now `close()` our socket. To +overcome this, we stopped the local `remoted` daemon: + +```shell +sudo pkill -SIGSTOP remoted +``` + +> **NOTE:** This "fight" will only happen for connections from the USB Ethernet interface. Connections from the created +> trusted tunnel device should work without any interference. + +Now we can initiate a pair: + +```shell +# will trigger a pair (wait user consent) +pymobiledevice3 remote create-listener +``` + +For the QUIC tunnel we still require additional work. Until then, you may reuse the [existing tunnel created by the +macOS](#reusing-the-macos-trusted-tunnel). + +### Accessing services over RemoteXPC + +Almost every command of `pymobiledevice` now receives an optional `--rsd`, allowing us to communicate the same old +services over RSD. Please notice some of them, such as all the developer services will now only be accessible this way. From b14e9b0216c633db0a6fc05847297014be4701a0 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 24 Jul 2023 09:16:32 +0300 Subject: [PATCH 136/234] pyproject: bump version to 2.1.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba1298409..1dd121ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.0.4" +version = "2.1.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 259394db823085e6dab223ee8a69d57385e4bdab Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 26 Jul 2023 13:40:52 +0300 Subject: [PATCH 137/234] instruments: add `simulate_location` for iOS>=17.0 --- README.md | 2 ++ pymobiledevice3/cli/developer.py | 28 ++++++++++++++++++- .../dvt/instruments/location_simulation.py | 18 ++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 pymobiledevice3/services/dvt/instruments/location_simulation.py diff --git a/README.md b/README.md index a0ff6bd9f..0979111af 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,8 @@ There is A LOT you may do on the device using `pymobiledevice3`. This is just a * The following will assume the DeveloperDiskImage is already mounted: * Simulate an `x y` location: * `pymobiledevice3 developer simulate-location set x y` + * Or the following for iOS>=17.0: + * `pymobiledevice3 developer dvt simulate-location set --rsd HOST PORT -- x y` * Taking a screenshot from the device: * `pymobiledevice3 developer dvt screenshot /path/to/screen.png` * View detailed process list (including ppid, uid, guid, sandboxed, etc...): diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 7924bf98c..7fcb397ec 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -21,7 +21,6 @@ from pymobiledevice3.exceptions import DeviceAlreadyInUseError, DvtDirListError, ExtractingStackshotError, \ UnrecognizedSelectorError from pymobiledevice3.lockdown import LockdownClient -from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.remote.core_device.app_service import AppServiceService from pymobiledevice3.remote.core_device.device_info import DeviceInfoService from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService @@ -37,6 +36,7 @@ from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo from pymobiledevice3.services.dvt.instruments.energy_monitor import EnergyMonitor from pymobiledevice3.services.dvt.instruments.graphics import Graphics +from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation from pymobiledevice3.services.dvt.instruments.network_monitor import ConnectionDetectionEvent, NetworkMonitor from pymobiledevice3.services.dvt.instruments.notifications import Notifications from pymobiledevice3.services.dvt.instruments.process_control import ProcessControl @@ -910,6 +910,32 @@ def dvt_har(service_provider: LockdownClient): tap.channel.receive_message() +@dvt.group('simulate-location') +def dvt_simulate_location(): + """ simulate-location options. """ + pass + + +@dvt_simulate_location.command('clear', cls=Command) +def dvt_simulate_location_clear(service_provider: LockdownClient): + """ clear simulated location """ + with DvtSecureSocketProxyService(service_provider) as dvt: + LocationSimulation(dvt).stop() + + +@dvt_simulate_location.command('set', cls=Command) +@click.argument('latitude', type=click.FLOAT) +@click.argument('longitude', type=click.FLOAT) +def dvt_simulate_location_set(service_provider: LockdownClient, latitude, longitude): + """ + set a simulated location. + try: + ... set -- 40.690008 -74.045843 for liberty island + """ + with DvtSecureSocketProxyService(service_provider) as dvt: + LocationSimulation(dvt).simulate_location(latitude, longitude) + + @developer.group() def core_device(): """ core-device options """ diff --git a/pymobiledevice3/services/dvt/instruments/location_simulation.py b/pymobiledevice3/services/dvt/instruments/location_simulation.py new file mode 100644 index 000000000..79c635e2c --- /dev/null +++ b/pymobiledevice3/services/dvt/instruments/location_simulation.py @@ -0,0 +1,18 @@ +import logging + +from pymobiledevice3.services.remote_server import MessageAux + + +class LocationSimulation: + IDENTIFIER = 'com.apple.instruments.server.services.LocationSimulation' + + def __init__(self, dvt): + self.logger = logging.getLogger(__name__) + self._channel = dvt.make_channel(self.IDENTIFIER) + + def simulate_location(self, latitude: float, longitude: float) -> None: + self._channel.simulateLocationWithLatitude_longitude_(MessageAux().append_obj(latitude).append_obj(longitude)) + self._channel.receive_plist() + + def stop(self) -> None: + self._channel.stopLocationSimulation() From 6d03c4a4b2df3ffc0e2e26a18753de140391335e Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 26 Jul 2023 14:17:16 +0300 Subject: [PATCH 138/234] pyproject: bump version to 2.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1dd121ddc..09c01dcb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.1.0" +version = "2.2.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 928f5601411e4731de04290abfad3a2f87b6a284 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 26 Jul 2023 17:34:27 +0300 Subject: [PATCH 139/234] migrate contact info to Discord --- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 562b5a100..d1500de66 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -https://gitter.im/pymobiledevice3/community. +https://discord.gg/52mZGC3JXJ. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75b0f986c..f38ebe4dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ If you would like to contribute, feel free to report issues, start new discussions, or create pull requests. You can -also contact us on gitter: +also contact us on Discord: -https://gitter.im/pymobiledevice3/community +https://discord.gg/52mZGC3JXJ From 9dd45fa486cea1b003f1c0fb95e869b3d1d8e2f6 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Tue, 1 Aug 2023 02:43:40 -0500 Subject: [PATCH 140/234] don't fail restore if sending restoresep fails --- pymobiledevice3/restore/recovery.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index 5d2dadf1f..ccadc17ff 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -380,8 +380,11 @@ def enter_restore(self): self.send_component_and_command('RestoreDeviceTree', 'devicetree') if self.build_identity.has_component('RestoreSEP'): - # send rsepfirmware and load it - self.send_component_and_command('RestoreSEP', 'rsepfirmware') + # attempt to send rsepfirmware and load it, otherwise continue + try: + self.send_component_and_command('RestoreSEP', 'rsepfirmware') + except USBError: + pass self.send_kernelcache() From ebdb5efe386e5ecaa375e02b1dafb89fcddba0bb Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 2 Aug 2023 08:04:47 +0300 Subject: [PATCH 141/234] pyproject: bump version to 2.2.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09c01dcb1..3d6b4e118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.2.0" +version = "2.2.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From b04092bedb47f6c67c23ad3ab8677c56709439fd Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 2 Aug 2023 09:13:32 +0300 Subject: [PATCH 142/234] crash_reports: fix `get_new_sysdiagnose()` for ios17 --- pymobiledevice3/services/crash_reports.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index 6a311e680..40617323a 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -1,5 +1,6 @@ import logging import posixpath +import time from typing import Generator, List from cmd2 import Cmd2ArgumentParser, with_argparser @@ -13,6 +14,9 @@ SYSDIAGNOSE_PROCESS_NAMES = ('sysdiagnose', 'sysdiagnosed') +# on iOS17, we need to wait for a moment before tryint to fetch the sysdiagnose archive +IOS17_SYSDIAGNOSE_DELAY = 1 + class CrashReportsManager: COPY_MOBILE_NAME = 'com.apple.crashreportcopymobile' @@ -152,6 +156,7 @@ def get_new_sysdiagnose(self, out: str, erase: bool = True) -> None: break self.afc.wait_exists(sysdiagnose_filename) + time.sleep(IOS17_SYSDIAGNOSE_DELAY) self.pull(out, entry=sysdiagnose_filename, erase=erase) From 90c7a791c5af08c0caa2c1f7c7fd847cfdfd8d0d Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 2 Aug 2023 10:44:38 +0300 Subject: [PATCH 143/234] device_link: ignore `backup_manifest.db` backup error (ios17 bug) --- pymobiledevice3/services/device_link.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymobiledevice3/services/device_link.py b/pymobiledevice3/services/device_link.py index b099a51b1..967941949 100644 --- a/pymobiledevice3/services/device_link.py +++ b/pymobiledevice3/services/device_link.py @@ -2,6 +2,7 @@ import datetime import shutil import struct +import warnings from pathlib import Path from pymobiledevice3.exceptions import PyMobileDevice3Exception @@ -9,6 +10,7 @@ SIZE_FORMAT = '>I' CODE_FORMAT = '>B' CODE_FILE_DATA = 0xc +CODE_ERROR_REMOTE = 0xb CODE_ERROR_LOCAL = 0x6 CODE_SUCCESS = 0 FILE_TRANSFER_TERMINATOR = b'\x00\x00\x00\x00' @@ -125,6 +127,12 @@ def upload_files(self, message): size, = struct.unpack(SIZE_FORMAT, self.service.recvall(struct.calcsize(SIZE_FORMAT))) code, = struct.unpack(CODE_FORMAT, self.service.recvall(struct.calcsize(CODE_FORMAT))) size -= struct.calcsize(CODE_FORMAT) + if code == CODE_ERROR_REMOTE: + # iOS 17 beta devices give this error for: backup_manifest.db + error_message = self.service.recvall(size).decode() + warnings.warn(f'Failed to fully upload: {file_name}. Device file name: {device_name}. Reason: ' + f'{error_message}') + continue assert code == CODE_SUCCESS self.status_response(0) From 413d5f349834a20a1e440df6c2a8c9aa3b30708a Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 2 Aug 2023 11:19:54 +0300 Subject: [PATCH 144/234] pyproject: bump version to 2.2.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3d6b4e118..b463da08c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.2.1" +version = "2.2.2" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 2d15a740e015105bbd52808256ffcda404b1bd27 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Thu, 3 Aug 2023 21:54:45 -0500 Subject: [PATCH 145/234] [recovery] dfu_enter_recovery: Don't expect recovery mode after iBSS On 8: old_nonce = self.device.irecv.ap_nonce From 5165191b85f0878459d2747a2cb22731de9c28d5 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:29:12 +0300 Subject: [PATCH 146/234] remotexpc_sniffer: include more data for each printed object --- misc/remotexpc_sniffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/remotexpc_sniffer.py b/misc/remotexpc_sniffer.py index fc03a3877..a49f4b1b7 100644 --- a/misc/remotexpc_sniffer.py +++ b/misc/remotexpc_sniffer.py @@ -151,7 +151,7 @@ def _handle_data_frame(self, stream: H2Stream, frame: DataFrame) -> None: if xpc_message is None: return - logger.info(f'As Python Object: {pformat(xpc_message)}') + logger.info(f'As Python Object (#{frame.stream_id}): {pformat(xpc_message)}') # print `pairingData` if exists, since it contains an inner struct if 'value' not in xpc_message: From 41b89ea23737977be87372e63ad7d2293c02082a Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:11:03 +0300 Subject: [PATCH 147/234] bonjour: close resources correctly --- pymobiledevice3/remote/bonjour.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pymobiledevice3/remote/bonjour.py b/pymobiledevice3/remote/bonjour.py index 75ec66ea4..9dfd4a324 100644 --- a/pymobiledevice3/remote/bonjour.py +++ b/pymobiledevice3/remote/bonjour.py @@ -1,4 +1,4 @@ -import itertools +import dataclasses import time from socket import AF_INET6, inet_ntop from typing import List @@ -25,15 +25,27 @@ def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: self.addresses.append(inet_ntop(AF_INET6, entry.address) + '%' + self.adapter.nice_name) -def query_bonjour(adapter: Adapter) -> RemotedListener: - zeroconf = Zeroconf(interfaces=[adapter.ips[0].ip[0]]) +@dataclasses.dataclass +class BonjourQuery: + zc: Zeroconf + service_browser: ServiceBrowser + listener: RemotedListener + + +def query_bonjour(adapter: Adapter) -> BonjourQuery: + zc = Zeroconf(interfaces=[adapter.ips[0].ip[0]]) listener = RemotedListener(adapter) - ServiceBrowser(zeroconf, '_remoted._tcp.local.', listener) - return listener + service_browser = ServiceBrowser(zc, '_remoted._tcp.local.', listener) + return BonjourQuery(zc, service_browser, listener) def get_remoted_addresses(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[str]: adapters = [adapter for adapter in get_adapters() if adapter.ips[0].is_IPv6] - listeners = [query_bonjour(adapter) for adapter in adapters] + bonjour_queries = [query_bonjour(adapter) for adapter in adapters] time.sleep(timeout) - return list(itertools.chain.from_iterable([listener.addresses for listener in listeners])) + addresses = [] + for bonjour_query in bonjour_queries: + addresses += bonjour_query.listener.addresses + bonjour_query.service_browser.cancel() + bonjour_query.zc.close() + return addresses From 8624c6ed20b6099e724ade28ebc355f5d0768a1f Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:12:57 +0300 Subject: [PATCH 148/234] MobileImageMounterService: fix typing in constructor --- pymobiledevice3/services/mobile_image_mounter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index bbdafe142..5bd4ea390 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -11,6 +11,7 @@ DeveloperModeIsNotEnabledError, InternalError, MessageNotSupportedError, MissingManifestError, NotMountedError, \ PyMobileDevice3Exception, UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.restore.tss import TSSRequest from pymobiledevice3.services.lockdown_service import LockdownService @@ -20,7 +21,7 @@ class MobileImageMounterService(LockdownService): SERVICE_NAME = 'com.apple.mobile.mobile_image_mounter' RSD_SERVICE_NAME = 'com.apple.mobile.mobile_image_mounter.shim.remote' - def __init__(self, lockdown: LockdownClient): + def __init__(self, lockdown: LockdownServiceProvider): if isinstance(lockdown, LockdownClient): super().__init__(lockdown, self.SERVICE_NAME) else: From 0bf015e2806ca90e68f64cbe96445d702a831ce2 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:11:46 +0300 Subject: [PATCH 149/234] heartbeat: fix `start()` when called using RSD --- pymobiledevice3/services/heartbeat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/services/heartbeat.py b/pymobiledevice3/services/heartbeat.py index 1430d6348..a34371340 100644 --- a/pymobiledevice3/services/heartbeat.py +++ b/pymobiledevice3/services/heartbeat.py @@ -21,7 +21,7 @@ def __init__(self, lockdown: LockdownServiceProvider): def start(self, interval=None): start = time.time() - service = self.lockdown.start_lockdown_service(self.SERVICE_NAME) + service = self.lockdown.start_lockdown_service(self.service_name) while True: response = service.recv_plist() From d483f98d5f50074633ef64d4e1b81dd46159b501 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:15:51 +0300 Subject: [PATCH 150/234] RemoteXPCConnection: add `shell()` --- pymobiledevice3/remote/remotexpc.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index e1507ddf6..7a10e981e 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -2,12 +2,15 @@ from socket import create_connection from typing import Generator, Mapping, Optional, Tuple +import IPython from construct import StreamError from hyperframe.frame import DataFrame, Frame, GoAwayFrame, HeadersFrame, RstStreamFrame, SettingsFrame, \ WindowUpdateFrame +from pygments import formatters, highlight, lexers from pymobiledevice3.exceptions import StreamClosedError -from pymobiledevice3.remote.xpc_message import XpcFlags, XpcWrapper, create_xpc_wrapper, decode_xpc_object +from pymobiledevice3.remote.xpc_message import XpcFlags, XpcInt64Type, XpcUInt64Type, XpcWrapper, create_xpc_wrapper, \ + decode_xpc_object # Extracted by sniffing `remoted` traffic via Wireshark DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS = 100 @@ -21,6 +24,13 @@ FILE_TRANSFER_CHANNEL = 2 REPLY_CHANNEL = 3 +SHELL_USAGE = """ +# This shell allows you to communicate directly with every RemoteXPC service. + +# For example, you can do the following: +resp = client.send_receive_request({"Command": "DoSomething"}) +""" + class RemoteXPCConnection: def __init__(self, address: Tuple[str, int]): @@ -84,6 +94,16 @@ def send_receive_request(self, data: Mapping): self.send_request(data, wanting_reply=True) return self.receive_response() + def shell(self) -> None: + IPython.embed( + header=highlight(SHELL_USAGE, lexers.PythonLexer(), + formatters.TerminalTrueColorFormatter(style='native')), + user_ns={ + 'client': self, + 'XpcInt64Type': XpcInt64Type, + 'XpcUInt64Type': XpcUInt64Type, + }) + def _do_handshake(self) -> None: self.sock.sendall(HTTP2_MAGIC) From f12b188553be5cae54f6fedb43f7ebe5da790756 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:56:01 +0300 Subject: [PATCH 151/234] RemoteXPCConnection: handle multiple large streams --- pymobiledevice3/remote/remotexpc.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index 7a10e981e..a61fee6b9 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -21,7 +21,6 @@ HTTP2_MAGIC = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' ROOT_CHANNEL = 1 -FILE_TRANSFER_CHANNEL = 2 REPLY_CHANNEL = 3 SHELL_USAGE = """ @@ -37,7 +36,7 @@ def __init__(self, address: Tuple[str, int]): self._previous_frame_data = b'' self.address = address self.sock: Optional[socket.socket] = None - self.next_message_id: Mapping[int: int] = {ROOT_CHANNEL: 0, FILE_TRANSFER_CHANNEL: 0, REPLY_CHANNEL: 0} + self.next_message_id: Mapping[int: int] = {ROOT_CHANNEL: 0, REPLY_CHANNEL: 0} self.peer_info = None def __enter__(self) -> 'RemoteXPCConnection': @@ -59,12 +58,22 @@ def send_request(self, data: Mapping, wanting_reply: bool = False) -> None: data, message_id=self.next_message_id[ROOT_CHANNEL], wanting_reply=wanting_reply) self.sock.sendall(DataFrame(stream_id=ROOT_CHANNEL, data=xpc_wrapper).serialize()) - def iter_file_chunks(self, total_size: int) -> Generator[bytes, None, None]: - self._open_channel(FILE_TRANSFER_CHANNEL, XpcFlags.FILE_TX_STREAM_RESPONSE) + def iter_file_chunks(self, total_size: int, file_idx: int = 0) -> Generator[bytes, None, None]: + stream_id = (file_idx + 1) * 2 + self._open_channel(stream_id, XpcFlags.FILE_TX_STREAM_RESPONSE) size = 0 while size < total_size: frame = self._receive_next_data_frame() - assert frame.stream_id == FILE_TRANSFER_CHANNEL + + if 'END_STREAM' in frame.flags: + continue + + if frame.stream_id != stream_id: + xpc_wrapper = XpcWrapper.parse(frame.data) + if xpc_wrapper.flags.FILE_TX_STREAM_REQUEST: + continue + + assert frame.stream_id == stream_id, f'got {frame.stream_id} instead of {stream_id}' size += len(frame.data) yield frame.data @@ -147,6 +156,10 @@ def _receive_next_data_frame(self) -> DataFrame: if not isinstance(frame, DataFrame): continue + if frame.stream_id % 2 == 0 and frame.body_len > 0: + self._send_frame(WindowUpdateFrame(stream_id=0, window_increment=frame.body_len)) + self._send_frame(WindowUpdateFrame(stream_id=frame.stream_id, window_increment=frame.body_len)) + return frame def _receive_frame(self) -> Frame: From 219c892f979113c61f6c1a50056a3323f1caad08 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:20:55 +0300 Subject: [PATCH 152/234] RemoteServiceDiscoveryService: add common members --- pymobiledevice3/lockdown.py | 6 +++--- pymobiledevice3/lockdown_service_provider.py | 5 +++++ pymobiledevice3/remote/remote_service_discovery.py | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 35e02bbf4..47eac952a 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -103,9 +103,8 @@ def _inner_reconnect_on_remote_close(*args, **kwargs): class LockdownClient(ABC, LockdownServiceProvider): def __init__(self, service: LockdownServiceConnection, 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): + label: str = DEFAULT_LABEL, system_buid: str = SYSTEM_BUID, pair_record: Mapping = None, + pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT): """ Create a LockdownClient instance @@ -118,6 +117,7 @@ def __init__(self, service: LockdownServiceConnection, host_id: str, identifier: :param pairing_records_cache_folder: Use the following location to search and save pair records :param port: lockdownd service port """ + super().__init__() self.logger = logging.getLogger(__name__) self.service = service self.identifier = identifier diff --git a/pymobiledevice3/lockdown_service_provider.py b/pymobiledevice3/lockdown_service_provider.py index 205307308..1e197d304 100644 --- a/pymobiledevice3/lockdown_service_provider.py +++ b/pymobiledevice3/lockdown_service_provider.py @@ -1,11 +1,16 @@ import logging from abc import abstractmethod +from typing import Optional from pymobiledevice3.exceptions import StartServiceError from pymobiledevice3.service_connection import LockdownServiceConnection class LockdownServiceProvider: + def __init__(self): + self.udid: Optional[str] = None + self.product_type: Optional[str] = None + @property @abstractmethod def product_version(self) -> str: diff --git a/pymobiledevice3/remote/remote_service_discovery.py b/pymobiledevice3/remote/remote_service_discovery.py index 654ef3c53..821e901f0 100644 --- a/pymobiledevice3/remote/remote_service_discovery.py +++ b/pymobiledevice3/remote/remote_service_discovery.py @@ -24,6 +24,7 @@ class RSDDevice: class RemoteServiceDiscoveryService(LockdownServiceProvider): def __init__(self, address: Tuple[str, int]): + super().__init__() self.service = RemoteXPCConnection(address) self.peer_info = None @@ -34,6 +35,8 @@ def product_version(self) -> str: def connect(self) -> None: self.service.connect() self.peer_info = self.service.receive_response() + self.udid = self.peer_info['Properties']['UniqueDeviceID'] + self.product_type = self.peer_info['Properties']['ProductType'] def start_lockdown_service_without_checkin(self, name: str) -> LockdownServiceConnection: return LockdownServiceConnection.create_using_tcp(self.service.address[0], self._get_service_port(name)) @@ -84,6 +87,10 @@ def __enter__(self) -> 'RemoteServiceDiscoveryService': def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.service.close() + def __repr__(self) -> str: + return (f'<{self.__class__.__name__} PRODUCT:{self.product_type} VERSION:{self.product_version} ' + f'UDID:{self.udid}>') + def _get_service_port(self, name: str) -> int: service = self.peer_info['Services'].get(name) if service is None: From 6f17036b1ecc4cd1adaad86f627567a407a64882 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:24:35 +0300 Subject: [PATCH 153/234] RemoteServiceDiscoveryService: prevent trigger `connect()` on creation --- pymobiledevice3/remote/remote_service.py | 1 + pymobiledevice3/remote/remote_service_discovery.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/remote_service.py b/pymobiledevice3/remote/remote_service.py index 2eeff3117..fd46bb1b0 100644 --- a/pymobiledevice3/remote/remote_service.py +++ b/pymobiledevice3/remote/remote_service.py @@ -14,6 +14,7 @@ def __init__(self, rsd: RemoteServiceDiscoveryService, service_name: str): def connect(self) -> None: self.service = self.rsd.start_remote_service(self.service_name) + self.service.connect() def __enter__(self): self.connect() diff --git a/pymobiledevice3/remote/remote_service_discovery.py b/pymobiledevice3/remote/remote_service_discovery.py index 821e901f0..4952bb18d 100644 --- a/pymobiledevice3/remote/remote_service_discovery.py +++ b/pymobiledevice3/remote/remote_service_discovery.py @@ -71,7 +71,6 @@ def start_lockdown_developer_service(self, name, escrow_bag: bytes = None) -> Lo def start_remote_service(self, name: str) -> RemoteXPCConnection: service = RemoteXPCConnection((self.service.address[0], self._get_service_port(name))) - service.connect() return service def start_service(self, name: str) -> Union[RemoteXPCConnection, LockdownServiceConnection]: From 5df626de81402426a0a5ce07ba5bd44ee26c8fd3 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:17:24 +0300 Subject: [PATCH 154/234] cli: fix `developer fetch-symbols` for ios17 --- pymobiledevice3/cli/developer.py | 65 +++++++++++-------- .../services/remote_fetch_symbols.py | 46 +++++++++++++ 2 files changed, 84 insertions(+), 27 deletions(-) create mode 100755 pymobiledevice3/services/remote_fetch_symbols.py diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 7fcb397ec..78646e1f9 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -13,6 +13,7 @@ import click from click.exceptions import MissingParameter, UsageError +from packaging.version import Version from pykdebugparser.pykdebugparser import PyKdebugParser from termcolor import colored @@ -21,6 +22,7 @@ from pymobiledevice3.exceptions import DeviceAlreadyInUseError, DvtDirListError, ExtractingStackshotError, \ UnrecognizedSelectorError from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.remote.core_device.app_service import AppServiceService from pymobiledevice3.remote.core_device.device_info import DeviceInfoService from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService @@ -43,6 +45,7 @@ from pymobiledevice3.services.dvt.instruments.screenshot import Screenshot from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap from pymobiledevice3.services.os_trace import OsTraceService +from pymobiledevice3.services.remote_fetch_symbols import RemoteFetchSymbolsService from pymobiledevice3.services.remote_server import RemoteServer from pymobiledevice3.services.screenshot import ScreenshotService from pymobiledevice3.services.simulate_location import DtSimulateLocation @@ -643,40 +646,48 @@ def fetch_symbols(): @fetch_symbols.command('list', cls=Command) @click.option('--color/--no-color', default=True) -def fetch_symbols_list(service_provider: LockdownClient, color: bool): +def fetch_symbols_list(service_provider: LockdownServiceProvider, color: bool): """ list of files to be downloaded """ - print_json(DtFetchSymbols(service_provider).list_files(), colored=color) + if Version(service_provider.product_version) < Version('17.0'): + print_json(DtFetchSymbols(service_provider).list_files(), colored=color) + else: + with RemoteFetchSymbolsService(service_provider) as fetch_symbols: + print_json([f.file_path for f in fetch_symbols.get_dsc_file_list()], colored=color) @fetch_symbols.command('download', cls=Command) @click.argument('out', type=click.Path(dir_okay=True, file_okay=False)) -def fetch_symbols_download(service_provider: LockdownClient, out): +def fetch_symbols_download(service_provider: LockdownServiceProvider, out): """ download the linker and dyld cache to a specified directory """ - fetch_symbols = DtFetchSymbols(service_provider) - files = fetch_symbols.list_files() - out = Path(out) - - if not os.path.exists(out): - os.makedirs(out) - - downloaded_files = set() - for i, file in enumerate(files): - if file.startswith('/'): - # trim root to allow relative download - file = file[1:] - file = out / file - - if file not in downloaded_files: - # first time the file was seen in list, means we can safely remove any old copy if any - file.unlink(missing_ok=True) - - downloaded_files.add(file) - file.parent.mkdir(parents=True, exist_ok=True) - with open(file, 'ab') as f: - # same file may appear twice, so we'll need to append data into it - logger.info(f'writing to: {file}') - fetch_symbols.get_file(i, f) + out = Path(out) + out.mkdir(parents=True, exist_ok=True) + + if Version(service_provider.product_version) < Version('17.0'): + fetch_symbols = DtFetchSymbols(service_provider) + files = fetch_symbols.list_files() + + downloaded_files = set() + + for i, file in enumerate(files): + if file.startswith('/'): + # trim root to allow relative download + file = file[1:] + file = out / file + + if file not in downloaded_files: + # first time the file was seen in list, means we can safely remove any old copy if any + file.unlink(missing_ok=True) + + downloaded_files.add(file) + file.parent.mkdir(parents=True, exist_ok=True) + with open(file, 'ab') as f: + # same file may appear twice, so we'll need to append data into it + logger.info(f'writing to: {file}') + fetch_symbols.get_file(i, f) + else: + with RemoteFetchSymbolsService(service_provider) as fetch_symbols: + fetch_symbols.download(out) @developer.group('simulate-location') diff --git a/pymobiledevice3/services/remote_fetch_symbols.py b/pymobiledevice3/services/remote_fetch_symbols.py new file mode 100755 index 000000000..51eef5ab4 --- /dev/null +++ b/pymobiledevice3/services/remote_fetch_symbols.py @@ -0,0 +1,46 @@ +import dataclasses +import uuid +from pathlib import Path +from typing import List + +from tqdm import tqdm + +from pymobiledevice3.remote.remote_service import RemoteService +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService + + +@dataclasses.dataclass +class DSCFile: + file_path: str + file_size: int + + +class RemoteFetchSymbolsService(RemoteService): + SERVICE_NAME = 'com.apple.dt.remoteFetchSymbols' + + def __init__(self, rsd: RemoteServiceDiscoveryService): + super().__init__(rsd, self.SERVICE_NAME) + + def get_dsc_file_list(self) -> List[DSCFile]: + files: List[DSCFile] = [] + response = self.service.send_receive_request({'XPCDictionary_sideChannel': uuid.uuid4(), 'DSCFilePaths': []}) + file_count = response['DSCFilePaths'] + for i in range(file_count): + response = self.service.receive_response()['DSCFilePaths'] + file_transfer = response['fileTransfer'] + expected_length = file_transfer['expectedLength'] + file_path = response['filePath'] + files.append(DSCFile(file_path=file_path, file_size=expected_length)) + return files + + def download(self, out: Path) -> None: + files = self.get_dsc_file_list() + for i, file in enumerate(files): + self.logger.info(f'Downloading {file}') + out_file = out / file.file_path[1:] # trim the "/" prefix + out_file.parent.mkdir(parents=True, exist_ok=True) + with open(out_file, 'wb') as f: + with tqdm(total=files[i].file_size, dynamic_ncols=True) as pb: + for chunk in self.service.iter_file_chunks(files[i].file_size, file_idx=i): + f.write(chunk) + pb.update(len(chunk)) From 76a3d37574fcbf0a21a8b488b954f181cd327b6a Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:19:14 +0300 Subject: [PATCH 155/234] remote: add `common.stop_remoted()` context-manager --- pymobiledevice3/remote/utils.py | 36 +++++++++++++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 37 insertions(+) create mode 100644 pymobiledevice3/remote/utils.py diff --git a/pymobiledevice3/remote/utils.py b/pymobiledevice3/remote/utils.py new file mode 100644 index 000000000..719010eb1 --- /dev/null +++ b/pymobiledevice3/remote/utils.py @@ -0,0 +1,36 @@ +import contextlib +import platform +from typing import Generator + +import psutil + +REMOTED_PATH = '/usr/libexec/remoted' + + +def _get_remoted_process() -> psutil.Process: + for process in psutil.process_iter(): + if process.pid == 0: + # skip kernel task + continue + if process.exe() == REMOTED_PATH: + return process + + +@contextlib.contextmanager +def stop_remoted() -> Generator[None, None, None]: + if platform.system() != 'Darwin': + # only Darwin systems require it + yield + return + + remoted = _get_remoted_process() + if remoted.status() == 'stopped': + # process already stopped, we don't need to do anything + yield + return + + remoted.suspend() + try: + yield + finally: + remoted.resume() diff --git a/requirements.txt b/requirements.txt index a3ff64d85..1887de920 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,3 +36,4 @@ srptools aioquic developer_disk_image>=0.0.2 opack +psutil From d3afea33650d14e42f5020e1da5ac9d1d40a5db7 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 08:23:29 +0300 Subject: [PATCH 156/234] core_device_tunnel_service: refactor `CoreDeviceTunnelService` Make `CoreDeviceTunnelService` extend `RemoteService` --- pymobiledevice3/remote/core_device_tunnel_service.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index b5bba282b..3943def37 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -107,11 +107,12 @@ def _encode_cdtunnel_packet(data: Mapping) -> bytes: return CDTunnelPacket.build({'body': json.dumps(data).encode()}) -class CoreDeviceTunnelService: +class CoreDeviceTunnelService(RemoteService): + SERVICE_NAME = 'com.apple.internal.dt.coredevice.untrusted.tunnelservice' WIRE_PROTOCOL_VERSION = 19 def __init__(self, rsd: RemoteServiceDiscoveryService): - self._logger = logging.getLogger(__name__) + super().__init__(rsd, self.SERVICE_NAME) self._sequence_number = 0 self._encrypted_sequence_number = 0 self.rsd = rsd @@ -126,7 +127,7 @@ def __init__(self, rsd: RemoteServiceDiscoveryService): self.signature = None def connect(self, autopair: bool = True) -> None: - self.service = self.rsd.start_remote_service('com.apple.internal.dt.coredevice.untrusted.tunnelservice') + super().connect() self.version = self.service.receive_response()['ServiceVersion'] self._attempt_pair_verify() @@ -158,15 +159,15 @@ async def start_quic_tunnel(self, private_key: RSAPrivateKey, secrets_log_file: host = self.service.address[0] port = parameters['port'] - self._logger.debug(f'Connecting to {host}:{port}') + self.logger.debug(f'Connecting to {host}:{port}') async with aioquic_connect( host, port, configuration=configuration, create_protocol=RemotePairingTunnel, ) as client: - self._logger.debug('quic connected') return await client.request_tunnel_establish() + self.logger.debug('quic connected') def save_pair_record(self) -> None: self.pair_record_path.write_bytes( From 4d5a6e7e40ace820ca2fd11b77871080acf10ab5 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 08:34:06 +0300 Subject: [PATCH 157/234] cli: add `get_device_list()` to remote subcommands --- pymobiledevice3/cli/remote.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 53c4224df..e07d8ed3b 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -1,5 +1,6 @@ import asyncio import logging +from typing import List, TextIO import click from cryptography.hazmat.primitives.asymmetric import rsa @@ -12,6 +13,15 @@ logger = logging.getLogger(__name__) +def get_device_list() -> List[RemoteServiceDiscoveryService]: + result = [] + for address in get_remoted_addresses(): + rsd = RemoteServiceDiscoveryService((address, RSD_PORT)) + rsd.connect() + result.append(rsd) + return result + + @click.group() def cli(): """ remote cli """ @@ -29,13 +39,12 @@ def remote_cli(): def browse(color: bool): """ browse devices using bonjour """ devices = [] - for address in get_remoted_addresses(): - with RemoteServiceDiscoveryService((address, RSD_PORT)) as rsd: - devices.append({'address': address, - 'port': RSD_PORT, - 'UniqueDeviceID': rsd.peer_info['Properties']['UniqueDeviceID'], - 'ProductType': rsd.peer_info['Properties']['ProductType'], - 'OSVersion': rsd.peer_info['Properties']['OSVersion']}) + for rsd in get_device_list(): + devices.append({'address': rsd.service.address[0], + 'port': RSD_PORT, + 'UniqueDeviceID': rsd.peer_info['Properties']['UniqueDeviceID'], + 'ProductType': rsd.peer_info['Properties']['ProductType'], + 'OSVersion': rsd.peer_info['Properties']['OSVersion']}) print_json(devices, colored=color) From 1a71bfcacf182a204f51d76f505d83db2522deb3 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 08:37:52 +0300 Subject: [PATCH 158/234] cli: use `stop_remoted()` when browsing for devices --- pymobiledevice3/cli/remote.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index e07d8ed3b..810385532 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -9,16 +9,18 @@ from pymobiledevice3.remote.bonjour import get_remoted_addresses from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService +from pymobiledevice3.remote.utils import stop_remoted logger = logging.getLogger(__name__) def get_device_list() -> List[RemoteServiceDiscoveryService]: result = [] - for address in get_remoted_addresses(): - rsd = RemoteServiceDiscoveryService((address, RSD_PORT)) - rsd.connect() - result.append(rsd) + with stop_remoted(): + for address in get_remoted_addresses(): + rsd = RemoteServiceDiscoveryService((address, RSD_PORT)) + rsd.connect() + result.append(rsd) return result From 89a1df6aa616b083e41383bb1e5e8c7ea79effc5 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 08:38:39 +0300 Subject: [PATCH 159/234] core_device_tunnel_service: fully implement a working tunnel --- pymobiledevice3/cli/remote.py | 48 +++++--- .../remote/core_device_tunnel_service.py | 116 +++++++++++++++--- requirements.txt | 4 +- 3 files changed, 130 insertions(+), 38 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 810385532..9b7f093e6 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -5,12 +5,17 @@ import click from cryptography.hazmat.primitives.asymmetric import rsa -from pymobiledevice3.cli.cli_common import RSDCommand, print_json +from pymobiledevice3.cli.cli_common import RSDCommand, print_json, prompt_device_list from pymobiledevice3.remote.bonjour import get_remoted_addresses -from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService from pymobiledevice3.remote.utils import stop_remoted +try: + from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service +except ImportError: + # isn't supported on Windows + pass + logger = logging.getLogger(__name__) @@ -57,21 +62,30 @@ def rsd_info(service_provider: RemoteServiceDiscoveryService, color: bool): print_json(service_provider.peer_info, colored=color) -@remote_cli.command('create-listener', cls=RSDCommand) -@click.option('-p', '--protocol', type=click.Choice(['quic', 'udp'])) -@click.option('--color/--no-color', default=True) -def create_listener(service_provider: RemoteServiceDiscoveryService, protocol: str, color: bool): - """ start a remote listener """ +async def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, secrets: TextIO) -> None: private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) with create_core_device_tunnel_service(service_provider, autopair=True) as service: - print_json(service.create_listener(private_key, protocol=protocol), colored=color) - - -@remote_cli.command('start-quic-tunnel', cls=RSDCommand) -@click.option('--color/--no-color', default=True) -def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, color: bool): + async with service.start_quic_tunnel(private_key, secrets_log_file=secrets) as tunnel_result: + if secrets is not None: + print(click.style('Secrets: ', bold=True, fg='magenta') + + click.style(secrets.name, bold=True, fg='white')) + print(click.style('Interface: ', bold=True, fg='yellow') + + click.style(tunnel_result.interface, bold=True, fg='white')) + print(click.style('RSD Address: ', bold=True, fg='yellow') + + click.style(tunnel_result.address, bold=True, fg='white')) + print(click.style('RSD Port: ', bold=True, fg='yellow') + + click.style(tunnel_result.port, bold=True, fg='white')) + print(click.style('Use the follow connection option:\n', bold=True, fg='yellow') + + click.style(f'--rsd {tunnel_result.address} {tunnel_result.port}', bold=True, fg='cyan')) + + while True: + # wait user input while the asyncio tasks execute + await asyncio.sleep(.5) + + +@remote_cli.command('start-quic-tunnel') +@click.option('--secrets', type=click.File('wt'), help='TLS keyfile for decrypting with Wireshark') +def cli_start_quic_tunnel(secrets: TextIO): """ start quic tunnel """ - logger.critical('This is a WIP command. Will only print the required parameters for the quic connection') - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - with create_core_device_tunnel_service(service_provider, autopair=True) as service: - print_json(asyncio.run(service.start_quic_tunnel(private_key)), colored=color) + rsd = prompt_device_list(get_device_list()) + asyncio.run(start_quic_tunnel(rsd, secrets), debug=True) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 3943def37..2e0df27e1 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -1,22 +1,28 @@ import asyncio import base64 import binascii +import dataclasses import hashlib import json -import logging import platform import plistlib +import struct +from asyncio import CancelledError from collections import namedtuple +from contextlib import asynccontextmanager, suppress from pathlib import Path +from socket import AF_INET6 from ssl import VerifyMode -from typing import List, Mapping, Optional, TextIO - -from aioquic.asyncio import QuicConnectionProtocol -from aioquic.asyncio.client import connect as aioquic_connect -from aioquic.asyncio.protocol import QuicStreamHandler -from aioquic.quic.configuration import QuicConfiguration -from aioquic.quic.connection import QuicConnection -from aioquic.quic.events import QuicEvent, StreamDataReceived +from typing import AsyncGenerator, List, Mapping, Optional, TextIO, cast + +import aiofiles +from aioquic_pmd3.asyncio import QuicConnectionProtocol +from aioquic_pmd3.asyncio.client import connect as aioquic_connect +from aioquic_pmd3.asyncio.protocol import QuicStreamHandler +from aioquic_pmd3.quic import packet_builder +from aioquic_pmd3.quic.configuration import QuicConfiguration +from aioquic_pmd3.quic.connection import QuicConnection +from aioquic_pmd3.quic.events import ConnectionTerminated, DatagramFrameReceived, QuicEvent, StreamDataReceived from construct import Const, Container, Enum, GreedyBytes, GreedyRange, Int8ul, Int16ub, Int64ul, Prefixed, Struct from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives._serialization import Encoding, PublicFormat @@ -26,14 +32,21 @@ from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF from opack import dumps +from pytun_pmd3 import TunTapDevice from srptools import SRPClientSession, SRPContext from srptools.constants import PRIME_3072, PRIME_3072_GEN from pymobiledevice3.ca import make_cert from pymobiledevice3.pair_records import create_pairing_records_cache_folder, generate_host_id +from pymobiledevice3.remote.remote_service import RemoteService from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type +LOOKBACK_HEADER = struct.pack('>I', AF_INET6) + +# The iOS device uses an MTU of 1500, so we'll have to increase the default QUIC MTU +packet_builder.PACKET_MAX_SIZE = 1452 # 1500 - 40byte ipv6 - 8 byte udp + PairingDataComponentType = Enum(Int8ul, METHOD=0x00, IDENTIFIER=0x01, @@ -83,30 +96,78 @@ class RemotePairingTunnel(QuicConnectionProtocol): - MTU = 1420 + MAX_QUIC_DATAGRAM = 14000 + MAX_IDLE_TIMEOUT = 30.0 + REQUESTED_MTU = 1420 def __init__(self, quic: QuicConnection, stream_handler: Optional[QuicStreamHandler] = None): super().__init__(quic, stream_handler) self._queue = asyncio.Queue() + self._keep_alive_task = None + self._tun_read_task = None + self.tun = None + + async def tun_read_task(self) -> None: + read_size = self.tun.mtu + len(LOOKBACK_HEADER) + async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f: + while True: + packet = await f.read(read_size) + assert packet.startswith(LOOKBACK_HEADER) + packet = packet.removeprefix(LOOKBACK_HEADER) + self._quic.send_datagram_frame(packet) + self.transmit() async def request_tunnel_establish(self) -> Mapping: stream_id = self._quic.get_next_available_stream_id() - # TODO: understand what this buffer should actually do + # pad the data with random data to force the MTU size correctly self._quic.send_datagram_frame(b'x' * 1024) self._quic.send_stream_data(stream_id, self._encode_cdtunnel_packet( - {'type': 'clientHandshakeRequest', 'mtu': self.MTU})) + {'type': 'clientHandshakeRequest', 'mtu': self.REQUESTED_MTU})) self.transmit() return await self._queue.get() + async def keep_alive_task(self, interval: float) -> None: + while True: + await self.ping() + await asyncio.sleep(interval) + + def start_tunnel(self, address: str, mtu: int) -> None: + self.tun = TunTapDevice() + self.tun.mtu = mtu + self.tun.addr6 = address + self._keep_alive_task = asyncio.create_task(self.keep_alive_task(self.MAX_IDLE_TIMEOUT / 2)) + self._tun_read_task = asyncio.create_task(self.tun_read_task()) + + async def stop_tunnel(self) -> None: + self._keep_alive_task.cancel() + self._tun_read_task.cancel() + with suppress(CancelledError): + await self._keep_alive_task + with suppress(CancelledError): + await self._tun_read_task + self.tun = None + def quic_event_received(self, event: QuicEvent) -> None: - if isinstance(event, StreamDataReceived): + if isinstance(event, ConnectionTerminated): + self.close() + elif isinstance(event, StreamDataReceived): self._queue.put_nowait(json.loads(CDTunnelPacket.parse(event.data).body)) + elif isinstance(event, DatagramFrameReceived): + self.tun.write(LOOKBACK_HEADER + event.data) @staticmethod def _encode_cdtunnel_packet(data: Mapping) -> bytes: return CDTunnelPacket.build({'body': json.dumps(data).encode()}) +@dataclasses.dataclass +class TunnelResult: + interface: str + address: str + port: int + client: RemotePairingTunnel + + class CoreDeviceTunnelService(RemoteService): SERVICE_NAME = 'com.apple.internal.dt.coredevice.untrusted.tunnelservice' WIRE_PROTOCOL_VERSION = 19 @@ -146,14 +207,20 @@ def create_listener(self, private_key: RSAPrivateKey, protocol: str = 'quic') -> response = self._send_receive_encrypted_request(request) return response['createListener'] - async def start_quic_tunnel(self, private_key: RSAPrivateKey, secrets_log_file: Optional[TextIO] = None) -> Mapping: + @asynccontextmanager + async def start_quic_tunnel(self, private_key: RSAPrivateKey, secrets_log_file: Optional[TextIO] = None) \ + -> AsyncGenerator[TunnelResult, None]: parameters = self.create_listener(private_key, protocol='quic') cert = make_cert(private_key, private_key.public_key()) - configuration = QuicConfiguration(alpn_protocols=['RemotePairingTunnelProtocol'], - is_client=True, - certificate=cert, - private_key=private_key, - verify_mode=VerifyMode.CERT_NONE) + configuration = QuicConfiguration( + alpn_protocols=['RemotePairingTunnelProtocol'], + is_client=True, + certificate=cert, + private_key=private_key, + verify_mode=VerifyMode.CERT_NONE, + max_datagram_frame_size=RemotePairingTunnel.MAX_QUIC_DATAGRAM, + idle_timeout=RemotePairingTunnel.MAX_IDLE_TIMEOUT + ) configuration.secrets_log_file = secrets_log_file host = self.service.address[0] @@ -166,8 +233,17 @@ async def start_quic_tunnel(self, private_key: RSAPrivateKey, secrets_log_file: configuration=configuration, create_protocol=RemotePairingTunnel, ) as client: - return await client.request_tunnel_establish() self.logger.debug('quic connected') + client = cast(RemotePairingTunnel, client) + await client.wait_connected() + handshake_response = await client.request_tunnel_establish() + client.start_tunnel(handshake_response['clientParameters']['address'], + handshake_response['clientParameters']['mtu']) + try: + yield TunnelResult( + client.tun.name, handshake_response['serverAddress'], handshake_response['serverRSDPort'], client) + finally: + await client.stop_tunnel() def save_pair_record(self) -> None: self.pair_record_path.write_bytes( diff --git a/requirements.txt b/requirements.txt index 1887de920..7714ce0ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,9 @@ zeroconf ifaddr hyperframe srptools -aioquic +aioquic-pmd3>=0.0.1 ; platform_system != "Windows" developer_disk_image>=0.0.2 opack psutil +pytun-pmd3>=0.0.2 ; platform_system != "Windows" +aiofiles From ac18f9fc0c36ed313d8a3d958df5d2d1ede687b5 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 9 Aug 2023 08:27:25 +0300 Subject: [PATCH 160/234] cli: add `remote shell` --- pymobiledevice3/cli/remote.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 9b7f093e6..5a02da1fb 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -89,3 +89,11 @@ def cli_start_quic_tunnel(secrets: TextIO): """ start quic tunnel """ rsd = prompt_device_list(get_device_list()) asyncio.run(start_quic_tunnel(rsd, secrets), debug=True) + + +@remote_cli.command('shell', cls=RSDCommand) +@click.argument('service') +def cli_shell(service_provider: RemoteServiceDiscoveryService, service: str): + """ start an ipython shell for interacting with given service """ + with service_provider.start_remote_service(service) as service: + service.shell() From 10b6b56a0451a224f59fbd2012eff6b1d3fe08c8 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 09:27:02 +0300 Subject: [PATCH 161/234] cli: avoid prompting when only one device on `start-quic-tunnel` --- pymobiledevice3/cli/remote.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 5a02da1fb..8b8d992e3 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -87,7 +87,14 @@ async def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, sec @click.option('--secrets', type=click.File('wt'), help='TLS keyfile for decrypting with Wireshark') def cli_start_quic_tunnel(secrets: TextIO): """ start quic tunnel """ - rsd = prompt_device_list(get_device_list()) + devices = get_device_list() + if not devices: + print('No device could be found') + return + if len(devices) == 1: + rsd = devices[0] + else: + rsd = prompt_device_list(devices) asyncio.run(start_quic_tunnel(rsd, secrets), debug=True) From 9aa863774a7886bcffb8310a523d0fb2c0d0e9d3 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 10:53:36 +0300 Subject: [PATCH 162/234] requirements: `cryptography>=41.0.1` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7714ce0ef..d8e33d91b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ requests cmd2 packaging pygnuutils>=0.0.7 -cryptography>=35.0.0 +cryptography>=41.0.1 pycrashreport>=1.0.6 fastapi[all] uvicorn>=0.15.0 From 1918c1fd43ee6de97abdd2d9144b5c51e6b913e6 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 14:43:13 +0300 Subject: [PATCH 163/234] pyproject: bump version to 2.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b463da08c..83149b9c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.2.2" +version = "2.3.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From b290bce3fba41e27c23c10fde8cb739e38c7e16c Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 10 Aug 2023 15:30:14 +0300 Subject: [PATCH 164/234] docs: update documentation regarding RSD --- NEWS.md | 14 ++++++++++++++ README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++-- misc/RemoteXPC.md | 36 +---------------------------------- 3 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 NEWS.md diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 000000000..25baaf26f --- /dev/null +++ b/NEWS.md @@ -0,0 +1,14 @@ +# News + +## 10/08/2023 + +Features: + +- Trying to work with iOS >= 17.0 devices? + See [Working with developer tools (iOS >= 17.0)](#working-with-developer-tools-ios--170) + +## 28/06/2023 + +API Changes: + +- Migrating from `pymobiledevice3<2.0.0`? See [this API change](README.md#python-api) \ No newline at end of file diff --git a/README.md b/README.md index 0979111af..e146bc07e 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,13 @@ [![Pypi version](https://img.shields.io/pypi/v/pymobiledevice3.svg)](https://pypi.org/project/pymobiledevice3/ "PyPi package") [![Downloads](https://static.pepy.tech/personalized-badge/pymobiledevice3?period=total&units=none&left_color=grey&right_color=blue&left_text=Downloads)](https://pepy.tech/project/pymobiledevice3) +- [News](#news) - [Description](#description) - [Installation](#installation) - * [Lower iOS versions (<13)](#lower-ios-versions---13-) + * [Lower iOS versions (<13)](#lower-ios-versions-13) - [Usage](#usage) + * [Python API](#python-api) + * [Working with developer tools (iOS >= 17.0)](#working-with-developer-tools-ios--170) * [Example](#example) - [The bits and bytes](#the-bits-and-bytes) * [Lockdown services](#lockdown-services) @@ -16,6 +19,10 @@ - [Instruments messages](#instruments-messages) - [Contributing](#contributing) +# News + +See [NEWS](NEWS.md). + # Description `pymobiledevice3` is a pure python3 implementation for working with iDevices (iPhone, etc...). This means this tool is @@ -124,6 +131,7 @@ Commands: processes processes cli profile profile options provision privision options + remote remote options restore restore options springboard springboard options syslog syslog options @@ -131,7 +139,9 @@ Commands: webinspector webinspector options ``` -Or import the modules and use the API yourself: +## Python API + +You could also import the modules and use the API yourself: ```python from pymobiledevice3.lockdown import create_using_usbmux @@ -143,6 +153,40 @@ for line in SyslogService(lockdown=lockdown).watch(): print(line) ``` +## Working with developer tools (iOS >= 17.0) + +> **NOTE:** Currently, this is only supported on macOS + +Starting at iOS 17.0, Apple introduced the new CoreDevice framework to work with iOS devices. This framework relies on +the [RemoteXPC](misc/RemoteXPC.md) protocol. In order to communicate with the developer services you'll be required to +first create [trusted tunnel](misc/RemoteXPC.md#trusted-tunnel) as follows: + +```shell +sudo python3 -m pymobiledevice3 remote start-quic-tunnel +``` + +The root permissions are required since this will create a new TUN/TAP device which is a high privilege operation. +The output should be something similar to: + +``` +Interface: utun6 +RSD Address: fd7b:e5b:6f53::1 +RSD Port: 64337 +Use the follow connection option: +--rsd fd7b:e5b:6f53::1 64337 +``` + +Now, (almost) all of pymobiledevice3 accept an additional `--rsd` option for connecting to the service over this new +tunnel. You can now try to execute any of them as follows: + +```shell +# Accessing the DVT services +python3 -m pymobiledevice3 developer dvt ls / --rsd fd7b:e5b:6f53::1 64337 + +# Or any of the "normal" ones +python3 -m pymobiledevice3 syslog live --rsd fd7b:e5b:6f53::1 64337 +``` + ## Example A recorded example for using a variety of features can be viewed at: diff --git a/misc/RemoteXPC.md b/misc/RemoteXPC.md index 5bf35cd3b..cd2b792e8 100644 --- a/misc/RemoteXPC.md +++ b/misc/RemoteXPC.md @@ -789,41 +789,7 @@ The response is just what the invoked function returned. ## Using `pymobiledevice3` as a client -### Handshake & Pairing - -`pymobiledevice3` also includes its own implementation of connecting to `remoted` and performing an RSD handshake. -You can try it yourself: - -```shell -pymobiledevice3 remote rsd-info --rsd HOST PORT -``` - -However, it seems trying to connect from the same host will resolve in a "fight" between `remoted` -and `pymobiledevice3`. The following message will appear on device syslog: - -``` -2023-07-23 22:11:18.608538 remoted{remoted}[50] : ncmhost-3> Canceling existing connection to replace it -``` - -This will trigger a `close()` on `remoted`'s FD, so it'll try to re-connect - which will now `close()` our socket. To -overcome this, we stopped the local `remoted` daemon: - -```shell -sudo pkill -SIGSTOP remoted -``` - -> **NOTE:** This "fight" will only happen for connections from the USB Ethernet interface. Connections from the created -> trusted tunnel device should work without any interference. - -Now we can initiate a pair: - -```shell -# will trigger a pair (wait user consent) -pymobiledevice3 remote create-listener -``` - -For the QUIC tunnel we still require additional work. Until then, you may reuse the [existing tunnel created by the -macOS](#reusing-the-macos-trusted-tunnel). +See [main documentation](/README.md#working-with-developer-tools-ios--170) for details. ### Accessing services over RemoteXPC From c0470bd855ea94481e552567d25cdde58ddae43f Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 13 Aug 2023 14:33:17 +0300 Subject: [PATCH 165/234] utils: skip zombie processes in `_get_remoted_process()` --- pymobiledevice3/remote/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/remote/utils.py b/pymobiledevice3/remote/utils.py index 719010eb1..7e4799258 100644 --- a/pymobiledevice3/remote/utils.py +++ b/pymobiledevice3/remote/utils.py @@ -12,8 +12,11 @@ def _get_remoted_process() -> psutil.Process: if process.pid == 0: # skip kernel task continue - if process.exe() == REMOTED_PATH: - return process + try: + if process.exe() == REMOTED_PATH: + return process + except psutil.ZombieProcess: + continue @contextlib.contextmanager From 7190801b72f39d40601202d444f0becbb3c0f749 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 13 Aug 2023 16:37:55 +0300 Subject: [PATCH 166/234] pyproject: bump version to 2.3.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 83149b9c0..18ea49b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.3.0" +version = "2.3.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 24ca0341f2c93686208d80950b354b79c014428a Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 14 Aug 2023 10:33:26 +0300 Subject: [PATCH 167/234] exceptions: add `AccessDeniedError` --- pymobiledevice3/exceptions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index 6e6ca02e7..ff5722be6 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -10,7 +10,8 @@ 'MissingValueError', 'PasscodeRequiredError', 'AmfiError', 'DeviceHasPasscodeSetError', 'NotificationTimeoutError', 'DeveloperModeError', 'ProfileError', 'IRecvError', 'IRecvNoDeviceConnectedError', 'NoDeviceSelectedError', 'MessageNotSupportedError', 'InvalidServiceError', 'InspectorEvaluateError', - 'LaunchingApplicationError', 'BadCommandError', 'BadDevError', 'ConnectionFailedError', 'CoreDeviceError' + 'LaunchingApplicationError', 'BadCommandError', 'BadDevError', 'ConnectionFailedError', 'CoreDeviceError', + 'AccessDeniedError' ] @@ -288,3 +289,8 @@ class AppInstallError(PyMobileDevice3Exception): class CoreDeviceError(PyMobileDevice3Exception): pass + + +class AccessDeniedError(PyMobileDevice3Exception): + """ Need extra permissions to execute this command """ + pass From e1a51265529c73a851652685ab4e843dfd21a1b9 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 14 Aug 2023 10:35:06 +0300 Subject: [PATCH 168/234] utils: raise `psutil.AccessDenied` as `AccessDeniedError` --- pymobiledevice3/remote/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/utils.py b/pymobiledevice3/remote/utils.py index 7e4799258..2b56152cc 100644 --- a/pymobiledevice3/remote/utils.py +++ b/pymobiledevice3/remote/utils.py @@ -4,6 +4,8 @@ import psutil +from pymobiledevice3.exceptions import AccessDeniedError + REMOTED_PATH = '/usr/libexec/remoted' @@ -32,7 +34,10 @@ def stop_remoted() -> Generator[None, None, None]: yield return - remoted.suspend() + try: + remoted.suspend() + except psutil.AccessDenied: + raise AccessDeniedError() try: yield finally: From 4036d9f08db89d88b75ccb9a34d3ceb6bc80024e Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 14 Aug 2023 10:35:34 +0300 Subject: [PATCH 169/234] cli: tell user to use `sudo` on `AccessDeniedError` --- pymobiledevice3/__main__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index a2e5f2976..c337101ba 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -29,10 +29,10 @@ from pymobiledevice3.cli.syslog import cli as syslog_cli from pymobiledevice3.cli.usbmux import cli as usbmux_cli from pymobiledevice3.cli.webinspector import cli as webinspector_cli -from pymobiledevice3.exceptions import ConnectionFailedError, DeveloperModeError, DeveloperModeIsNotEnabledError, \ - DeviceHasPasscodeSetError, InternalError, InvalidServiceError, MessageNotSupportedError, MissingValueError, \ - NoDeviceConnectedError, NoDeviceSelectedError, NotPairedError, PairingDialogResponsePendingError, \ - PasswordRequiredError, SetProhibitedError, UserDeniedPairingError +from pymobiledevice3.exceptions import AccessDeniedError, ConnectionFailedError, DeveloperModeError, \ + DeveloperModeIsNotEnabledError, DeviceHasPasscodeSetError, InternalError, InvalidServiceError, \ + MessageNotSupportedError, MissingValueError, NoDeviceConnectedError, NoDeviceSelectedError, NotPairedError, \ + PairingDialogResponsePendingError, PasswordRequiredError, SetProhibitedError, UserDeniedPairingError coloredlogs.install(level=logging.INFO) @@ -94,6 +94,8 @@ def cli(): return except PasswordRequiredError: logger.error('Device is password protected. Please unlock and retry') + except AccessDeniedError: + logger.error('This command requires root privileges. Consider retrying with "sudo".') except BrokenPipeError: traceback.print_exc() From 25282b0553d6c2d4411214c089f4aa862c8490a9 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 14 Aug 2023 12:54:53 +0300 Subject: [PATCH 170/234] notifications: update list from ios17 and sort --- .../resources/firmware_notifications.py | 6 +- pymobiledevice3/resources/notifications.txt | 1065 +++++++++-------- 2 files changed, 559 insertions(+), 512 deletions(-) diff --git a/pymobiledevice3/resources/firmware_notifications.py b/pymobiledevice3/resources/firmware_notifications.py index 73f5c608c..b22be5017 100644 --- a/pymobiledevice3/resources/firmware_notifications.py +++ b/pymobiledevice3/resources/firmware_notifications.py @@ -1,6 +1,7 @@ import logging import os import plistlib +from typing import List import click import coloredlogs @@ -15,8 +16,9 @@ def get_notifications(): return f.read().decode().split('\n') -def save_notifications(notifications): +def save_notifications(notifications: List[str]): with open(NOTIFICATIONS_FILENAME, 'wb') as f: + notifications.sort() f.write('\n'.join(notifications).encode()) @@ -54,7 +56,7 @@ def main(root_fs): logging.info(f'adding notification: {notification}') notifications.add(notification) - save_notifications(notifications) + save_notifications(list(notifications)) if __name__ == '__main__': diff --git a/pymobiledevice3/resources/notifications.txt b/pymobiledevice3/resources/notifications.txt index 639a2f7be..6d488933f 100644 --- a/pymobiledevice3/resources/notifications.txt +++ b/pymobiledevice3/resources/notifications.txt @@ -1,550 +1,595 @@ -com.apple.networkextension.app-paths-changed -com.apple.ap.adprivacyd.iTunesActiveStorefrontDidChangeNotification -com.apple.ManagedConfiguration.profileListChanged -com.apple.mobileslideshow.ICPLStateChanged +ABAddressBookMeCardChangeDistributedNotification +ACDAccountStoreDidChangeNotification +AFAssistantEnablementDidChangeDarwinNotification +AFLanguageCodeDidChangeDarwinNotification +AppleDatePreferencesChangedNotification +AppleKeyboardsPreferencesChangedNotification +AppleLanguagePreferencesChangedNotification +AppleNumberPreferencesChangedNotification +ApplePreferredContentSizeCategoryChangedNotification +AppleTimePreferencesChangedNotification +BYSetupAssistantFinishedDarwinNotification +CKAccountChangedNotification +CKIdentityUpdateNotification +CNContactStoreDidChangeNotification +CNContactStoreMeContactDidChangeNotification +CNFavoritesChangedExternallyNotification +CSLDisableWristDetectionChangedNotification +CalSyncClientBeginningMultiSave +CalSyncClientFinishedMultiSave +ConnectedGymPreferencesChangedNotification EKNotificationCountChangedExternallyNotification -com.apple.tv.updateAppVisibility -com.apple.bluetooth.connection -com.apple.duetexpertd.ATXAnchorModel.invalidate.ChargerConnectedAnchor -com.apple.LaunchServices.ApplicationsChanged -kCalEventOccurrenceCacheChangedNotification -com.apple.MobileAsset.ProactiveEventTrackerAssets.ma.new-asset-installed -com.apple.family.family_updated -com.apple.mobilecal.invitationalertschanged -com.apple.LoginKit.isLoggedIn -com.apple.ams.privateListeningChanged -com.apple.coreaudio.RoutingConfiguration -com.apple.siri.ShortcutsCloudKitAccountAddedNotification -com.apple.security.publickeyavailable -com.apple.springboard.finishedstartup +FMFDevicesChangedNotification +FMFMeDeviceChangedNotification +FMLDevicesChangedNotification +FMLFollowersChangedNotification +FMLMeDeviceChangedNotification +FitnessPlusPlanCoachingDefaultsUpdatedNotification +HKHealthDaemonActiveDataCollectionWillStartNotification +HKHealthDaemonActiveWorkoutServersDidUpdateNotification +INVoocabularyChangedNotification +MFNanoMailImportantBridgeSettingHasChangedDarwinNotification +MISProvisioningProfileInstalled +MISProvisioningProfileRemoved +MPStoreClientTokenDidChangeNotification +NILocalDeviceStartedInteractingWithTokenNotification +NanoLifestylePreferencesChangedNotification +NewCarrierNotification +NewOperatorNotification +NoteContextDarwinNotificationWithLoggedChanges +PCPreferencesDidChangeNotification +RTLocationsOfInterestDidChangeNotification +SBApplicationNotificationStateChanged +SLSharedWithYouAppSettingHasChanged +SLSharedWithYouSettingHasChanged +SUPreferencesChangedNotification +SeymourWorkoutPlanChanged +SignificantTimeChangeNotification +UIAccessibilityInvertColorsChanged +VMStoreSetTokenNotification +VT Phrase Type changed +VVMessageWaitingFallbackNotification +_CDPWalrusStateChangeDarwinNotification +_CalDatabaseChangedNotification +__ABDataBaseChangedByOtherProcessNotification +com.apple.AOSNotification.FMIPStateDidChange +com.apple.AirTunes.DACP.device-prevent-playback +com.apple.AirTunes.DACP.devicevolume +com.apple.AirTunes.DACP.devicevolumechanged +com.apple.AirTunes.DACP.mutetoggle +com.apple.AirTunes.DACP.nextitem +com.apple.AirTunes.DACP.pause com.apple.AirTunes.DACP.play -com.apple.softwareupdate.autoinstall.startInstall -com.apple.ProtectedCloudStorage.updatedKeys -com.apple.nanomusic.sync.defaults -kAFPreferencesDidChangeDarwinNotification -com.apple.siri.koa.donate -com.apple.softwareupdateservicesd.activity.splatAutoScan +com.apple.AirTunes.DACP.previtem +com.apple.AirTunes.DACP.repeatadv +com.apple.AirTunes.DACP.shuffletoggle com.apple.AirTunes.DACP.volumedown -com.apple.appstored.ActivitySubEntitlementsCacheUpdated -com.apple.tv.appRemoved -com.apple.mobilecal.preference.notification.weekStart -com.apple.mobileipod.keeplocalstatechanged -com.apple.accessibility.cache.invert.colors -com.apple.duetexpertd.ATXAnchorModel.invalidate.BluetoothConnectedAnchor -CNContactStoreMeContactDidChangeNotification -com.apple.duetexpertd.ms.carplaydisconnect -com.apple.duetexpertd.ATXScreenUnlockUpdateSource -com.apple.mobile.lockdown.activation_state -com.apple.kvs.store-did-change.com.apple.iBooks -com.apple.eventkit.preference.notification.UnselectedCalendarIdentifiersForFocusMode -com.apple.security.cloudkeychain.forceupdate -com.apple.datamigrator.migrationDidFinish -com.apple.assistant.app_vocabulary -com.apple.voicetrigger.enablePolicyChanged -com.apple.duetexpertd.ATXAnchorModel.ChargerConnectedAnchor -com.apple.security.view-ready.SE-PTC -com.apple.MobileAsset.VoiceTriggerAssetsIPad.ma.cached-metadata-updated -com.apple.duetexpertd.ms.nowplayingplay -com.apple.security.octagon.peer-changed -com.apple.softwareupdateservicesd.SUCoreConfigScheduledScan -com.apple.ManagedConfiguration.webContentFilterChanged -AppleKeyboardsPreferencesChangedNotification -com.apple.mobile.keybagd.first_unlock -com.apple.carkit.capabilities-changed -com.apple.NanoPhotos.Library.changed -SUPreferencesChangedNotification -com.apple.kvs.store-did-change.com.apple.cloudsettings.keyboard -com.apple.security.octagon.joined-with-bottle +com.apple.AirTunes.DACP.volumeup +com.apple.AppleMediaServices.deviceOffersChanged +com.apple.BiometricKit.passcodeGracePeriodChanged +com.apple.CallHistoryPluginHelper.launchnotification +com.apple.Carousel.wristStateChanged +com.apple.CloudSubscriptionFeature.Changed +com.apple.ContinuityKeyBoard.enabled +com.apple.DuetHeuristic-BM.shutdowsoon +com.apple.EscrowSecurityAlert.record +com.apple.EscrowSecurityAlert.reset com.apple.EscrowSecurityAlert.server -com.apple.telephonyutilities.callservicesd.fakeoutgoingmessage -com.apple.duetexpertd.donationmonitor.intent +com.apple.GeoServices.PreferencesSync.SettingsChanged +com.apple.GeoServices.navigation.started +com.apple.GeoServices.navigation.stopped +com.apple.LaunchServices.ApplicationsChanged +com.apple.LaunchServices.applicationRegistered +com.apple.LaunchServices.applicationUnregistered +com.apple.LockdownMode.accountChanged +com.apple.LoginKit.isLoggedIn +com.apple.MCX._managementStatusChangedForDomains +com.apple.ManagedConfiguration.managedAppsChanged +com.apple.ManagedConfiguration.profileListChanged +com.apple.ManagedConfiguration.webContentFilterChanged +com.apple.ManagedConfiguration.webContentFilterTypeChanged +com.apple.MediaRemote.lockScreenControlsDidChange com.apple.MediaRemote.nowPlayingActivePlayersIsPlayingDidChange -com.apple.kvs.store-did-change.com.apple.sleepd -com.apple.duetexpertd.mm.bluetoothconnected -com.apple.security.view-change.PCS -com.apple.trial.NamespaceUpdate.SIRI_VALUE_INFERENCE_CONTACT_RESOLUTION -com.apple.cloud.quota.simulate.vfs.almostfull -com.apple.dmd.iCloudAccount.didChange -com.apple.system.batterysavermode.first_time -com.apple.idscredentials.idslaunchnotification -com.apple.triald.wake -com.apple.system.lowpowermode.auto_disabled -com.apple.UsageTrackingAgent.registration.application -com.apple.system.batterysavermode +com.apple.MediaRemote.nowPlayingApplicationIsPlayingDidChange +com.apple.MediaRemote.nowPlayingInfoDidChange +com.apple.MobileAsset.AppleKeyServicesCRL.new-asset-installed +com.apple.MobileAsset.AutoAssetAtomicNotification^ATOMIC_INSTANCE_DOWNLOADED +com.apple.MobileAsset.CoreTextAssets.ma.cached-metadata-updated +com.apple.MobileAsset.CoreTextAssets.ma.new-asset-installed +com.apple.MobileAsset.EmbeddedSpeech.ma.new-asset-installed +com.apple.MobileAsset.Font7.ma.cached-metadata-updated +com.apple.MobileAsset.SecureElementServiceAssets.ma.cached-metadata-updated +com.apple.MobileAsset.SecureElementServiceAssets.ma.new-asset-installed +com.apple.MobileAsset.SpeechEndpointAssets.cached-metadata-updated +com.apple.MobileAsset.SpeechEndpointAssets.ma.cached-metadata-updated +com.apple.MobileAsset.TTSAXResourceModelAssets.ma.new-asset-installed +com.apple.MobileAsset.TimeZoneUpdate.ma.cached-metadata-updated +com.apple.MobileAsset.TimeZoneUpdate.ma.new-asset-installed +com.apple.MobileAsset.VoiceServices.CustomVoice.ma.new-asset-installed +com.apple.MobileAsset.VoiceServices.GryphonVoice.ma.new-asset-installed +com.apple.MobileAsset.VoiceServices.VoiceResources.ma.new-asset-installed com.apple.MobileAsset.VoiceServices.VoiceResources.new-asset-installed -com.apple.MobileAsset.VoiceTriggerHSAssetsIPad.ma.new-asset-installed -AppleDatePreferencesChangedNotification -com.apple.duetexpertd.updateDefaultsDueToRelevantHomeScreenConfigUpdate -com.apple.homed.AppleTVAccessoryAdded -com.apple.photostream.idslaunchnotification -SLSharedWithYouSettingHasChanged -com.apple.MobileAsset.VoiceTriggerAssetsMarsh.ma.new-asset-installed -com.apple.duetexpertd.ATXMMAppPredictor.WiredAudioDeviceDisconnectedAnchor -com.apple.pasteboard.notify.changed -com.apple.system.config.network_change -com.apple.ProtectedCloudStorage.mobileBackupStateChange -com.apple.corerecents.iCloudAccountChanged -com.apple.duetexpertd.donationmonitor.activity -com.apple.idsremoteurlconnection.idslaunchnotification -com.apple.security.octagon.trust-status-change -com.apple.icloud.searchparty.accessoryDidPair -MISProvisioningProfileInstalled -com.apple.ap.adprivacyd.iTunesActiveAccountDidChangeNotification -com.apple.cloud.quota.simulate.vfs.notfull -com.apple.softwareupdateservicesd.activity.installAlert +com.apple.MobileAsset.VoiceServicesVocalizerVoice.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerAssets.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerAssets.ma.cached-metadata-updated com.apple.MobileAsset.VoiceTriggerAssets.ma.new-asset-installed -com.apple.assistant.sync_needed -com.apple.spotlightui.prefschanged -com.apple.assistant.sync_homekit_now -com.apple.AirTunes.DACP.pause -com.apple.StoreServices.StorefrontChanged -com.apple.smartcharging.defaultschanged -com.apple.AirTunes.DACP.volumeup -com.apple.MobileAsset.VoiceTriggerHSAssetsWatch.ma.cached-metadata-updated -com.apple.mobileipod.displayvalueschanged -com.apple.coreduetd.remoteDeviceChange -com.apple.powerui.smartcharge -com.apple.MobileAsset.VoiceServices.VoiceResources.ma.new-asset-installed +com.apple.MobileAsset.VoiceTriggerAssets.new-asset-installed +com.apple.MobileAsset.VoiceTriggerAssetsIPad.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerAssetsIPad.ma.new-asset-installed +com.apple.MobileAsset.VoiceTriggerAssetsMarsh.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerAssetsMarsh.ma.new-asset-installed +com.apple.MobileAsset.VoiceTriggerAssetsStudioDisplay.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerAssetsStudioDisplay.ma.new-asset-installed +com.apple.MobileAsset.VoiceTriggerAssetsWatch.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerAssetsWatch.ma.new-asset-installed +com.apple.MobileAsset.VoiceTriggerAssetsWatch.new-asset-installed +com.apple.MobileAsset.VoiceTriggerHSAssets.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerHSAssets.ma.new-asset-installed com.apple.MobileAsset.VoiceTriggerHSAssetsIPad.ma.cached-metadata-updated -com.apple.MusicLibrary.importFinished-/var/mobile/Media/iTunes_Control/iTunes/MediaLibrary.sqlitedb -com.apple.mobiletimerd.chargetest -com.apple.powermanagement.idlesleeppreventers -com.apple.duetexpertd.clientModelRefreshBlendingLayer -com.apple.nanoregistry.paireddevicedidchangeversion -com.apple.duetexpertd.feedbackavailable -com.apple.contacts.clientDidDisplayFavorites -com.apple.appstored.NewsSubEntitlementsCacheUpdated -com.apple.coreduetd.nearbydeviceschanged -com.apple.networkextension.apps-changed -AppleLanguagePreferencesChangedNotification -CalSyncClientBeginningMultiSave -com.apple.nanoregistry.devicedidpair -com.apple.sleepd.ids.test -com.apple.voiceservices.notification.voice-update +com.apple.MobileAsset.VoiceTriggerHSAssetsIPad.ma.new-asset-installed +com.apple.MobileAsset.VoiceTriggerHSAssetsWatch.ma.cached-metadata-updated +com.apple.MobileAsset.VoiceTriggerHSAssetsWatch.ma.new-asset-installed +com.apple.MobileBackup.backgroundCellularAccessChanged +com.apple.MobileSoftwareUpdate.OSVersionChanged +com.apple.Music-AllowsCellularDataDownloads +com.apple.NanoPhotos.Library.changed +com.apple.OTACrashCopier.SubmissionPreferenceChanged +com.apple.Preferences.ChangedRestrictionsEnabledStateNotification +com.apple.Preferences.ResetPrivacyWarningsNotification +com.apple.ProtectedCloudStorage.mobileBackupStateChange +com.apple.ProtectedCloudStorage.rollBackupDisabled +com.apple.ProtectedCloudStorage.rollIfAged +com.apple.ProtectedCloudStorage.rollNow +com.apple.ProtectedCloudStorage.test.mobileBackupStateChange +com.apple.ProtectedCloudStorage.updatedKeys com.apple.ProximityControl.LockScreenDiscovery +com.apple.SOSEngine.SOSNotifyContactsReasonCinnamon +com.apple.SOSEngine.SOSNotifyContactsReasonKappa +com.apple.SOSEngine.SOSNotifyContactsReasonMandrake +com.apple.SOSEngine.SOSNotifyContactsReasonNewton +com.apple.SOSEngine.SOSNotifyContactsReasonSOSTrigger +com.apple.SafariShared.Assistant.reload_plugin +com.apple.SensorKit.als +com.apple.SensorKit.deviceUsageReport +com.apple.SensorKit.mediaEvents +com.apple.SensorKit.messagesUsageReport +com.apple.SensorKit.phoneUsageReport +com.apple.SensorKit.visits +com.apple.Sharing.prefsChanged +com.apple.SiriTTSTrainingAgent.taskEvent.cancelled +com.apple.SiriTTSTrainingAgent.taskEvent.done +com.apple.SiriTTSTrainingAgent.taskEvent.event +com.apple.SiriTTSTrainingAgent.taskEvent.failed +com.apple.SiriTTSTrainingAgent.taskEvent.running +com.apple.SiriTTSTrainingAgent.taskEvent.submitted +com.apple.SiriTTSTrainingAgent.taskEvent.undefined +com.apple.SoftwareUpdate.SUPreferencesChanged +com.apple.StoreServices.StorefrontChanged +com.apple.SynthesisProvider.updatedVoices +com.apple.TVRemoteCore.connectionRequested +com.apple.UsageTrackingAgent.registration.application +com.apple.UsageTrackingAgent.registration.now-playing +com.apple.UsageTrackingAgent.registration.video +com.apple.UsageTrackingAgent.registration.web-domain +com.apple.VideoSubscriberAccount.DidRegisterSubscription +com.apple.VideosUI.PlayHistoryUpdatedNotification +com.apple.VideosUI.StoreAcquisitionCrossProcessNotification +com.apple.VideosUI.UpNextRequestDidFinishNotification +com.apple.accessibility.cache.darken.system.colors.enabled +com.apple.accessibility.cache.differentiate.without.color com.apple.accessibility.cache.enhance.background.contrast -com.apple.tv.TVWidgetExtension.Register -com.apple.siri.cloud.synch.changed -com.apple.system.lowpowermode -com.apple.rapport.prefsChanged -kKeepAppsUpToDateEnabledChangedNotification -com.apple.powermanagement.restartpreventers -com.apple.shortcuts.daemon-wakeup-request -com.apple.jett.switch.environmentChange.idms.complete -com.apple.kvs.store-did-change.com.apple.cloudsettings.international -com.apple.telephonyutilities.callservicesdaemon.voicemailcallended -com.apple.nanophotos.prefs.LibraryCollectionTargetMapData-changed -com.apple.AirTunes.DACP.device-prevent-playback -com.apple.appletv.backgroundstate -com.apple.duetexpertd.ATXMMAppPredictor.CarPlayDisconnectedAnchor -com.apple.nearfield.handoff.terminal -com.apple.icloud.fmip.lostmode.enable -com.apple.AOSNotification.FMIPStateDidChange -com.apple.navd.backgroundCommute.startPredicting -com.apple.system.clock_set -com.apple.ProtectedCloudStorage.rollBackupDisabled +com.apple.accessibility.cache.enhance.text.legibility +com.apple.accessibility.cache.invert.colors +com.apple.accessibility.cache.prefers.horizontal.text com.apple.accessibility.cache.reduce.motion -com.apple.mobile.storage_unmounted -com.apple.ap.aes -com.apple.kvs.store-did-change.com.apple.cloudsettings.controlcenter -com.apple.itunesstored.accountschanged -com.apple.siri.inference.coreduet-context -com.apple.mobileipod.noncontentspropertieschanged -com.apple.bookmarks.BookmarksFileChanged -com.apple.locationd.vehicle.exit -com.apple.mobilemail.afc.poll -com.apple.locationd.vehicle.disconnected -com.apple.cmfsyncagent.kvstorechange -com.apple.SafariShared.Assistant.reload_plugin -RTLocationsOfInterestDidChangeNotification -com.apple.ttsasset.NewAssetNotification +com.apple.accessibility.classic.wob.status +com.apple.accessibility.commandandcontrol.status +com.apple.accessibility.enhance.background.contrast.status +com.apple.accessibility.pointer.increased.contrast +com.apple.accessibility.prefers.horizontal.text +com.apple.accessibility.reduce.motion.status +com.apple.accessibility.reduce.white.point +com.apple.accessibility.voiceovertouch.status +com.apple.accessibility.zoomtouch.status +com.apple.accessories.connection.MFi4AccessoryDisconnected +com.apple.accessories.connection.passedMFi4Auth +com.apple.ams.privateListeningChanged +com.apple.ams.provision-biometrics +com.apple.ap.adprivacyd.canceltasks com.apple.ap.adprivacyd.deviceKnowledge -com.apple.purplebuddy.setupdone -HKHealthDaemonActiveWorkoutServersDidUpdateNotification -com.apple.MobileAsset.VoiceTriggerAssets.ma.cached-metadata-updated +com.apple.ap.adprivacyd.iTunesActiveAccountDidChangeNotification +com.apple.ap.adprivacyd.iTunesActiveStorefrontDidChangeNotification +com.apple.ap.adprivacyd.launch com.apple.ap.adprivacyd.reconcile -com.apple.icloud.findmydeviced.findkit.magSafe.removed -com.apple.siri.ShortcutsCloudKitAccountModifiedNotification -com.apple.pairedsync.syncDidComplete -kFZVCAppBundleIdentifierLaunchNotification -com.apple.parsecd.bag +com.apple.appletv.backgroundstate +com.apple.appstored.ActivitySubEntitlementsCacheUpdated +com.apple.appstored.AppStoreSubEntitlementsCacheUpdated +com.apple.appstored.HWBundleSubEntitlementsCacheUpdated +com.apple.appstored.MusicSubEntitlementsCacheUpdated +com.apple.appstored.NewsSubEntitlementsCacheUpdated +com.apple.appstored.PodcastSubEntitlementsCacheUpdated com.apple.appstored.TVSubEntitlementsCacheUpdated -com.apple.symptoms.materialLinkQualityChange -com.apple.MobileAsset.VoiceTriggerAssets.new-asset-installed -com.apple.MobileAsset.VoiceServicesVocalizerVoice.ma.cached-metadata-updated -com.apple.duetexpertd.ATXAnchorModel.invalidate.CarPlayConnectedAnchor -com.apple.navd.wakeUpForHypothesisUpdate -com.apple.duetexpertd.mm.bluetoothdisconnect -com.apple.duetexpertd.ATXMMAppPredictor.IdleTimeEndAnchor -com.apple.mobilecal.timezonechanged -com.apple.proactive.information.source.weather -com.apple.MobileAsset.VoiceTriggerAssets.cached-metadata-updated -com.apple.duetexpertd.ATXMMAppPredictor.BluetoothConnectedAnchor -com.apple.voicetrigger.RemoteDarwin.ConnectionChanged -com.apple.kvs.store-did-change.com.apple.cloudsettings.displays -com.apple.mobileipod-prefsChanged -com.apple.CallHistoryPluginHelper.launchnotification -com.apple.exchangesyncd.ping -com.apple.homehubd.endpointDeactivated -com.apple.wcd.wake-up -com.apple.media.entities.siri_data_changed -kFZACAppBundleIdentifierLaunchNotification -com.apple.voicemail.changed -PCPreferencesDidChangeNotification -CSLDisableWristDetectionChangedNotification -com.apple.MobileAsset.TimeZoneUpdate.ma.new-asset-installed -com.apple.springboard.lockstate -com.apple.homed.televisionAccessoryAdded -com.apple.locationd.vehicular.changed.toVehicular -com.apple.duetexpertd.ms.nowplayingpause -com.apple.mobileslideshow.PLNotificationKeepOriginalsChanged -com.apple.voicetrigger.XPCRestarted -com.apple.duetexpertd.ATXAnchorModel.invalidate.WiredAudioDeviceConnectedAnchor +com.apple.appstored.iCloudSubEntitlementsCacheUpdated +com.apple.assistant.app_vocabulary +com.apple.assistant.siri_settings_did_change +com.apple.assistant.speech-capture.finished com.apple.assistant.sync_data_changed -EKFeatureSetDidChangeNotification -com.apple.MobileAsset.VoiceTriggerAssetsIPad.ma.new-asset-installed -com.apple.system.powersources.criticallevel -com.apple.tcc.access.changed -com.apple.locationd.appreset +com.apple.assistant.sync_homekit_now +com.apple.assistant.sync_homekit_urgent +com.apple.assistant.sync_needed +com.apple.atc.xpc.runkeeplocaltask +com.apple.audio.AOP.enable +com.apple.awd.launch.wifi +com.apple.bluetooth.WirelessSplitterOn +com.apple.bluetooth.accessory-authentication.success +com.apple.bluetooth.connection +com.apple.bluetooth.daemonStarted +com.apple.bluetooth.pairing +com.apple.bluetooth.state +com.apple.bookmarks.BookmarksFileChanged +com.apple.calendar.database.preference.notification.kCalPreferredDaysToSyncKey +com.apple.calendar.database.preference.notification.suggestEventLocations +com.apple.callhistory.RecentsClearedNotification +com.apple.callhistory.notification.calls-changed +com.apple.callhistorysync.idslaunchnotification +com.apple.carkit.capabilities-changed +com.apple.carkit.carplay-attached +com.apple.cddcommunicator.batteryChanged +com.apple.cddcommunicator.nwchanged +com.apple.cddcommunicator.pluginChanged +com.apple.cddcommunicator.thermalChanged +com.apple.chatkit.groups.siri_data_changed +com.apple.cloud.quota.simulate.vfs.almostfull +com.apple.cloud.quota.simulate.vfs.notfull +com.apple.cloudd.pcsIdentityUpdate-com.apple.ProactivePredictionsBackup +com.apple.cloudrecents.kvstorechange +com.apple.cmfsyncagent.kvstorechange +com.apple.cmfsyncagent.storedidchangeexternally +com.apple.commcenter.DataSettingsChangedNotification +com.apple.commcenter.InternationalRoamingEDGE.changed +com.apple.contacts.clientDidDisplayFavorites +com.apple.coreaudio.RoutingConfiguration com.apple.coreaudio.borealisTrigger -logging tasks have changed -com.apple.proactive.PersonalizationPortrait.namedEntitiesDidChangeMeaningfully -com.apple.kvs.store-did-change.com.apple.cloudsettings.appearance -com.apple.mobiletimerd.resttest -com.apple.suggestions.settingsChanged -com.apple.VideosUI.PlayHistoryUpdatedNotification -com.apple.thermalmonitor.ageAwareMitigationsEnabled -com.apple.media.podcasts.siri_data_changed -com.apple.voicemail.ReloadService -com.apple.duetexpertd.ATXMMAppPredictor.WiredAudioDeviceConnectedAnchor -com.apple.mobiletimerd.waketest -com.apple.AirTunes.DACP.shuffletoggle -com.apple.MediaRemote.lockScreenControlsDidChange -CNContactStoreDidChangeNotification -com.apple.accessibility.cache.darken.system.colors.enabled -com.apple.AirTunes.DACP.devicevolume -com.apple.softwareupdateservicesd.activity.autoDownload +com.apple.coreaudio.speechDetectionVAD.created +com.apple.coreduet.client-needs-help.coreduetd +com.apple.coreduet.idslaunchnotification +com.apple.coreduetd.knowledgebase.launch.duetexpertd +com.apple.coreduetd.nearbydeviceschanged +com.apple.coreduetd.remoteDeviceChange +com.apple.coremedia.carplayisconnected +com.apple.corerecents.iCloudAccountChanged +com.apple.corespotlight.developer.ReindexAllItems +com.apple.corespotlight.developer.ReindexAllItemsWithIdentifiers +com.apple.crashreporter.auto_submit_preference_changed com.apple.da.tasking_changed -com.apple.fairplayd.resync-fpkeybag -com.apple.SensorKit.deviceUsageReport -com.apple.isp.backcamerapower -com.apple.homed.user-cloud-share.wake.com.apple.siri.zonesharing -com.apple.ProtectedCloudStorage.test.mobileBackupStateChange -com.apple.triald.new-experiment -com.apple.private.SensorKit.pedometer.stridecalibration +com.apple.dataaccess.checkHolidayCalendarAccount +com.apple.dataaccess.ping +com.apple.datamigrator.datamigrationcompletecontinuerestore +com.apple.datamigrator.migrationDidFinish +com.apple.devicemanagementclient.longLivedTokenChanged +com.apple.dmd.budget.didChange +com.apple.dmd.iCloudAccount.didChange +com.apple.duet.expertcenter.appRefresh +com.apple.duetbm.internalSettingsChanged +com.apple.duetexpertd.ATXAnchorModel.BluetoothConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.CarPlayConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.ChargerConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.IdleTimeEndAnchor +com.apple.duetexpertd.ATXAnchorModel.WiredAudioDeviceConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.invalidate.BluetoothConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.invalidate.CarPlayConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.invalidate.ChargerConnectedAnchor +com.apple.duetexpertd.ATXAnchorModel.invalidate.IdleTimeEndAnchor +com.apple.duetexpertd.ATXAnchorModel.invalidate.WiredAudioDeviceConnectedAnchor +com.apple.duetexpertd.ATXMMAppPredictor.BluetoothConnectedAnchor +com.apple.duetexpertd.ATXMMAppPredictor.BluetoothDisconnectedAnchor +com.apple.duetexpertd.ATXMMAppPredictor.CarPlayConnectedAnchor +com.apple.duetexpertd.ATXMMAppPredictor.CarPlayDisconnectedAnchor +com.apple.duetexpertd.ATXMMAppPredictor.IdleTimeEndAnchor +com.apple.duetexpertd.ATXMMAppPredictor.WiredAudioDeviceConnectedAnchor +com.apple.duetexpertd.ATXMMAppPredictor.WiredAudioDeviceDisconnectedAnchor +com.apple.duetexpertd.ATXScreenUnlockUpdateSource +com.apple.duetexpertd.appchangeprediction +com.apple.duetexpertd.appclipprediction +com.apple.duetexpertd.clientModelRefreshBlendingLayer +com.apple.duetexpertd.defaultsChanged +com.apple.duetexpertd.dockAppListCacheUpdate +com.apple.duetexpertd.donationmonitor.activity +com.apple.duetexpertd.donationmonitor.intent +com.apple.duetexpertd.feedbackavailable com.apple.duetexpertd.homeScreenPageConfigCacheUpdate -com.apple.homed.user-cloud-share.wake.com.apple.siri.data -com.apple.UsageTrackingAgent.registration.now-playing -com.apple.system.powermanagement.poweradapter -com.apple.MobileAsset.VoiceServices.CustomVoice.ma.new-asset-installed +com.apple.duetexpertd.mm.audiodisconnect +com.apple.duetexpertd.mm.bluetoothconnected +com.apple.duetexpertd.mm.bluetoothdisconnect +com.apple.duetexpertd.ms.carplayconnect +com.apple.duetexpertd.ms.carplaydisconnect +com.apple.duetexpertd.ms.nowplayingpause +com.apple.duetexpertd.ms.nowplayingplay +com.apple.duetexpertd.prefschanged +com.apple.duetexpertd.sportsTeamsChanged +com.apple.duetexpertd.updateDefaultsDueToRelevantHomeScreenConfigUpdate +com.apple.eventkit.preference.notification.UnselectedCalendarIdentifiersForFocusMode +com.apple.exchangesyncd.ping +com.apple.fairplayd.resync-fpkeybag +com.apple.family.family_updated +com.apple.fitness.FitnessAppInstalled +com.apple.gamepolicy.daemon.launch +com.apple.geoservices.siri_data_changed +com.apple.hangtracerd.htse_state_changed +com.apple.healthlite.SleepDetectedActivity +com.apple.healthlite.SleepSessionEndRequest +com.apple.homed.AppleTVAccessoryAdded +com.apple.homed.multi-user-status-changed +com.apple.homed.speakersConfiguredChanged +com.apple.homed.televisionAccessoryAdded +com.apple.homed.user-cloud-share.repair.wake.com.apple.applemediaservices.multiuser com.apple.homed.user-cloud-share.wake.com.apple.applemediaservices.multiuser -SBApplicationNotificationStateChanged -com.apple.kvs.store-did-change.com.apple.cloudsettings.pencil -com.apple.managedconfiguration.managedorginfochanged -com.apple.callhistory.RecentsClearedNotification -com.apple.wirelessproximity.launch -MFNanoMailImportantBridgeSettingHasChangedDarwinNotification -com.apple.mobiletimerd.wakeuptest -NewOperatorNotification -com.apple.MobileAsset.VoiceTriggerHSAssets.ma.cached-metadata-updated -com.apple.siri.cloud.storage.deleted -com.apple.commcenter.InternationalRoamingEDGE.changed -com.apple.alarm.label.siri_data_changed +com.apple.homed.user-cloud-share.wake.com.apple.applemediaservices.multiuser.qa +com.apple.homed.user-cloud-share.wake.com.apple.mediaservicesbroker.container +com.apple.homed.user-cloud-share.wake.com.apple.siri.data +com.apple.homed.user-cloud-share.wake.com.apple.siri.zonesharing +com.apple.homehubd.endpointActivated +com.apple.homehubd.endpointDeactivated +com.apple.homekit.sync-data-cache-updated +com.apple.icloud.FindMy.addMagSafeAccessory +com.apple.icloud.findmydeviced.findkit.magSafe.added +com.apple.icloud.findmydeviced.findkit.magSafe.attach +com.apple.icloud.findmydeviced.findkit.magSafe.detach +com.apple.icloud.findmydeviced.findkit.magSafe.removed +com.apple.icloud.findmydeviced.localActivationLockInfoChanged +com.apple.icloud.fmip.lostmode.enable +com.apple.icloud.fmip.siri_data_changed +com.apple.icloud.searchparty.accessoryDidPair +com.apple.icloud.searchparty.selfbeaconchanged +com.apple.icloudpairing.idslaunchnotification +com.apple.idscredentials.idslaunchnotification +com.apple.idsremoteurlconnection.idslaunchnotification com.apple.idstransfers.idslaunchnotification -com.apple.MobileAsset.AppleKeyServicesCRL.new-asset-installed -com.apple.aggregated.addaily.logging -com.apple.springboard.pluggedin -com.apple.managedconfiguration.restrictionchanged -com.apple.SensorKit.als -com.apple.powerlog.batteryServiceNotification -com.apple.system.logging.power_button_notification -com.apple.ManagedConfiguration.managedAppsChanged -com.apple.bluetooth.WirelessSplitterOn -com.apple.accessibility.component-change -com.apple.MobileAsset.Font7.ma.cached-metadata-updated -com.apple.appstored.AppStoreSubEntitlementsCacheUpdated -com.apple.itunesstored.autodownloaddefaultschange -com.apple.managedconfiguration.effectivesettingschanged -com.apple.mobileipod.librarychanged -com.apple.mobiletimerd.bedtimetest -com.apple.MobileAsset.CoreTextAssets.ma.new-asset-installed -com.apple.atc.xpc.runkeeplocaltask -com.apple.coremedia.carplayisconnected -com.apple.dataaccess.checkHolidayCalendarAccount -com.apple.MediaRemote.nowPlayingApplicationIsPlayingDidChange com.apple.imautomatichistorydeletionagent.prefchange -com.apple.proactive.queries.databaseChange -__ABDataBaseChangedByOtherProcessNotification -com.apple.pushproxy.idslaunchnotification +com.apple.intelligenceplatform.StorageSystem.Recovered +com.apple.iokit.hid.displayStatus +com.apple.isp.backcamerapower com.apple.isp.frontcamerapower -com.apple.security.view-change.SE-PTC -FMFDataUpdateCompleteNotification -AppleTimePreferencesChangedNotification -com.apple.spotlight.SyndicatedContentRefreshed -com.apple.duetexpertd.dockAppListCacheUpdate -com.apple.geoservices.siri_data_changed -com.apple.cmfsyncagent.storedidchangeexternally -com.apple.system.powersources.timeremaining -com.apple.MobileAsset.SpeechEndpointAssets.ma.cached-metadata-updated -com.apple.icloud.searchparty.selfbeaconchanged -com.apple.homed.user-cloud-share.wake.com.apple.applemediaservices.multiuser.qa -com.apple.AirTunes.DACP.previtem -com.apple.trial.NamespaceUpdate.SIRI_UNDERSTANDING_ASR_ASSISTANT -com.apple.accessories.connection.MFi4AccessoryDisconnected -com.apple.ManagedConfiguration.webContentFilterTypeChanged -com.apple.voicemail.VVVerifierCheckpointDictionaryChanged -com.apple.UsageTrackingAgent.registration.video -com.apple.carkit.carplay-attached com.apple.itunescloudd.artworkDownloadsDidCompleteNotification -com.apple.sleepd.cloudkit.reset -com.apple.SoftwareUpdate.SUPreferencesChanged +com.apple.itunesstored.accountschanged +com.apple.itunesstored.autodownloaddefaultschange +com.apple.itunesstored.invalidatebags +com.apple.jett.switch.environmentChange.idms.complete +com.apple.keystore.memento.effaced +com.apple.kvs.store-did-change.com.apple.LockdownMode +com.apple.kvs.store-did-change.com.apple.accessibility.livespeech +com.apple.kvs.store-did-change.com.apple.bluetooth.cloud.settings +com.apple.kvs.store-did-change.com.apple.cloudsettings.appearance +com.apple.kvs.store-did-change.com.apple.cloudsettings.controlcenter +com.apple.kvs.store-did-change.com.apple.cloudsettings.desktop +com.apple.kvs.store-did-change.com.apple.cloudsettings.displays +com.apple.kvs.store-did-change.com.apple.cloudsettings.gamecontroller +com.apple.kvs.store-did-change.com.apple.cloudsettings.general +com.apple.kvs.store-did-change.com.apple.cloudsettings.international +com.apple.kvs.store-did-change.com.apple.cloudsettings.keyboard +com.apple.kvs.store-did-change.com.apple.cloudsettings.mouse +com.apple.kvs.store-did-change.com.apple.cloudsettings.pencil +com.apple.kvs.store-did-change.com.apple.cloudsettings.sound +com.apple.kvs.store-did-change.com.apple.cloudsettings.trackpad +com.apple.kvs.store-did-change.com.apple.iBooks +com.apple.kvs.store-did-change.com.apple.reminders +com.apple.kvs.store-did-change.com.apple.sleepd +com.apple.language.changed +com.apple.livespeech.localprefschanged +com.apple.locationd.appreset +com.apple.locationd.authorization +com.apple.locationd.vehicle.connected +com.apple.locationd.vehicle.disconnected +com.apple.locationd.vehicle.exit +com.apple.locationd.vehicular.changed.toVehicular +com.apple.locationd/Prefs +com.apple.managedconfiguration.allowpasscodemodificationchanged +com.apple.managedconfiguration.effectivesettingschanged +com.apple.managedconfiguration.managedorginfochanged com.apple.managedconfiguration.passcodechanged -com.apple.springboard.hasBlankedScreen -com.apple.softwareupdateservicesd.activity.autoInstallEnd -com.apple.cloudrecents.kvstorechange -com.apple.softwareupdateservicesd.activity.presentBanner -com.apple.awd.launch.nfcd -com.apple.cddcommunicator.nwchanged -com.apple.proactive.PersonalizationPortrait.namedEntitiesInvalidated -com.apple.assistant.sync_homekit_urgent -com.apple.duetexpertd.appclipprediction -com.apple.sockpuppet.applications.updated +com.apple.managedconfiguration.restrictionchanged +com.apple.media.entities.siri_data_changed +com.apple.media.podcasts.siri_data_changed +com.apple.mediaaccessibility.displayFilterSettingsChanged +com.apple.mobile.disk_image_mounted +com.apple.mobile.keybagd.first_unlock +com.apple.mobile.keybagd.lock_status +com.apple.mobile.lockdown.BonjourPairingServiceChanged +com.apple.mobile.lockdown.BonjourServiceChanged +com.apple.mobile.lockdown.activation_state com.apple.mobile.lockdown.device_name_changed -CKIdentityUpdateNotification -com.apple.videos.migrationCompleted -com.apple.homed.multi-user-status-changed -com.apple.MobileAsset.ProactiveEventTrackerAssets.ma.cached-metadata-updated -com.apple.ap.adprivacyd.launch -com.apple.AirTunes.DACP.mutetoggle -com.apple.ap.adprivacyd.canceltasks -com.apple.TVRemoteCore.connectionRequested -com.apple.system.lowpowermode.first_time -com.apple.MobileAsset.CoreTextAssets.ma.cached-metadata-updated -com.apple.duetexpertd.mm.audiodisconnect -com.apple.hangtracerd.htse_state_changed -com.apple.sleep.sync.SleepScheduleDidChange +com.apple.mobile.storage_unmounted +com.apple.mobilecal.invitationalertschanged +com.apple.mobilecal.preference.notification.calendarsExcludedFromNotifications +com.apple.mobilecal.preference.notification.weekStart +com.apple.mobilecal.timezonechanged +com.apple.mobileipod-prefsChanged +com.apple.mobileipod.displayvalueschanged +com.apple.mobileipod.keeplocalstatechanged +com.apple.mobileipod.librarychanged +com.apple.mobileipod.libraryimportdidfinish +com.apple.mobileipod.noncontentspropertieschanged +com.apple.mobilemail.afc.poll +com.apple.mobileme.fmf1.allowFindMyFriendsModification +com.apple.mobileslideshow.ICPLStateChanged +com.apple.mobileslideshow.PLNotificationKeepOriginalsChanged +com.apple.mobiletimerd.bedtimetest +com.apple.mobiletimerd.chargetest +com.apple.mobiletimerd.diagnostics +com.apple.mobiletimerd.goodmorningtest +com.apple.mobiletimerd.reset +com.apple.mobiletimerd.resttest +com.apple.mobiletimerd.waketest +com.apple.mobiletimerd.wakeuptest +com.apple.nanomusic.sync.defaults +com.apple.nanophotos.prefs.LibraryCollectionTargetMapData-changed +com.apple.nanoregistry.devicedidpair +com.apple.nanoregistry.devicedidunpair +com.apple.nanoregistry.pairedSync.initialSyncDidComplete +com.apple.nanoregistry.paireddevicedidchangecapabilities +com.apple.nanoregistry.paireddevicedidchangeversion +com.apple.nanoregistry.watchdidbecomeactive +com.apple.navd.backgroundCommute.startPredicting +com.apple.navd.wakeUpForHypothesisUpdate +com.apple.nearfield.handoff.terminal +com.apple.networkextension.app-paths-changed +com.apple.networkextension.apps-changed +com.apple.networkextension.nehelper-init +com.apple.networkserviceproxy.reset +com.apple.nfcacd.multitag.state.change +com.apple.pairedsync.syncDidComplete +com.apple.parsec-fbf.FLUploadImmediately +com.apple.parsecd.bag +com.apple.parsecd.queries.clearData +com.apple.pasteboard.notify.changed +com.apple.pex.connections.focalappchanged +com.apple.photos.DidUpdateAutonamingUserFeedback +com.apple.photostream.idslaunchnotification +com.apple.powerlog.batteryServiceNotification +com.apple.powermanagement.idlesleeppreventers +com.apple.powermanagement.restartpreventers +com.apple.powermanagement.systempowerstate +com.apple.powermanagement.systemsleeppreventers com.apple.powerui.requiredFullCharge -com.apple.DuetHeuristic-BM.shutdowsoon -com.apple.datamigrator.datamigrationcompletecontinuerestore -NanoLifestylePreferencesChangedNotification -com.apple.icloud.findmydeviced.findkit.magSafe.added -com.apple.homekit.sync-data-cache-updated -com.apple.assistant.siri_settings_did_change +com.apple.powerui.smartcharge +com.apple.private.SensorKit.pedometer.stridecalibration +com.apple.proactive.PersonalizationPortrait.namedEntitiesInvalidated +com.apple.proactive.information.source.weather com.apple.proactive.queries.clearData -com.apple.corespotlight.developer.ReindexAllItemsWithIdentifiers -com.apple.corespotlight.developer.ReindexAllItems -com.apple.cddcommunicator.pluginChanged -com.apple.language.changed +com.apple.proactive.queries.databaseChange +com.apple.purplebuddy.setupdone +com.apple.purplebuddy.setupexited +com.apple.pushproxy.idslaunchnotification com.apple.rapport.CompanionLinkDeviceAdded -com.apple.voicetrigger.EarlyDetect -com.apple.voiceservices.trigger.asset-force-update -MPStoreClientTokenDidChangeNotification -com.apple.VideoSubscriberAccount.DidRegisterSubscription -com.apple.kvs.store-did-change.com.apple.cloudsettings.mouse -com.apple.MobileAsset.VoiceTriggerAssetsWatch.new-asset-installed -ConnectedGymPreferencesChangedNotification -_CalDatabaseChangedNotification -com.apple.powermanagement.systemsleeppreventers -com.apple.MobileAsset.EmbeddedSpeech.ma.new-asset-installed -com.apple.OTACrashCopier.SubmissionPreferenceChanged -com.apple.duetexpertd.ms.carplayconnect -com.apple.SensorKit.messagesUsageReport -com.apple.sleep.sync.SleepRecordDidChange -com.apple.healthlite.SleepSessionEndRequest -com.apple.welcomemat.finalizeMigratableApps -com.apple.Carousel.wristStateChanged -com.apple.networkserviceproxy.reset -ACDAccountStoreDidChangeNotification -com.apple.EscrowSecurityAlert.reset -CalSyncClientFinishedMultiSave -com.apple.mobiletimerd.reset -com.apple.mobileipod.libraryimportdidfinish -com.apple.security.publickeynotavailable -com.apple.Music-AllowsCellularDataDownloads -com.apple.siri.preheat.quiet -com.apple.AirTunes.DACP.nextitem -com.apple.duetbm.internalSettingsChanged -com.apple.parsecd.queries.clearData -com.apple.duetexpertd.ATXMMAppPredictor.CarPlayConnectedAnchor -MISProvisioningProfileRemoved +com.apple.rapport.prefsChanged +com.apple.remindd.nano_preferences_sync +com.apple.remotemanagement.accountsChanged +com.apple.sbd.kvstorechange +com.apple.screensharing.idslaunchnotification +com.apple.security.cloudkeychain.forceupdate +com.apple.security.cloudkeychainproxy.kvstorechange3 +com.apple.security.itembackup +com.apple.security.octagon.joined-with-bottle +com.apple.security.octagon.peer-changed +com.apple.security.octagon.trust-status-change +com.apple.security.publickeyavailable +com.apple.security.publickeynotavailable +com.apple.security.secureobjectsync.circlechanged +com.apple.security.secureobjectsync.holdlock +com.apple.security.secureobjectsync.viewschanged +com.apple.security.view-change.PCS +com.apple.security.view-change.SE-PTC +com.apple.security.view-ready.SE-PTC +com.apple.shortcuts.daemon-wakeup-request +com.apple.shortcuts.runner-prewarm-request +com.apple.siri.ShortcutsCloudKitAccountAddedNotification +com.apple.siri.ShortcutsCloudKitAccountModifiedNotification +com.apple.siri.client.state.DynamiteClientState.siri_data_changed +com.apple.siri.cloud.storage.deleted +com.apple.siri.cloud.synch.changed +com.apple.siri.history.deletion.requested +com.apple.siri.inference.audio-app-signals-update +com.apple.siri.koa.donate com.apple.siri.power.PowerContextPolicy.updated -com.apple.bluetooth.accessory-authentication.success -com.apple.dmd.budget.didChange -com.apple.icloud.fmip.siri_data_changed -com.apple.SensorKit.visits -NoteContextDarwinNotificationWithLoggedChanges -com.apple.mobile.keybagd.lock_status -com.apple.healthlite.SleepDetectedActivity -com.apple.locationd.vehicle.connected -com.apple.mobiletimerd.goodmorningtest -com.apple.UsageTrackingAgent.registration.web-domain -com.apple.assistant.speech-capture.finished -com.apple.awd.launch.wifi -com.apple.duetexpertd.ATXAnchorModel.WiredAudioDeviceConnectedAnchor -com.apple.devicemanagementclient.longLivedTokenChanged -com.apple.VideosUI.UpNextRequestDidFinishNotification -com.apple.MobileAsset.TTSAXResourceModelAssets.ma.new-asset-installed -com.apple.system.batterysavermode.auto_disabled -com.apple.MCX._managementStatusChangedForDomains +com.apple.siri.preheat.quiet +com.apple.siri.vocabulary.contacts_changed +com.apple.sleep.sync.SleepRecordDidChange +com.apple.sleep.sync.SleepScheduleDidChange +com.apple.sleep.sync.SleepSettingsDidChange +com.apple.sleepd.cloudkit.reset +com.apple.sleepd.diagnostics +com.apple.sleepd.ids.test +com.apple.smartcharging.defaultschanged +com.apple.sockpuppet.applications.updated +com.apple.softwareupdate.autoinstall.startInstall +com.apple.softwareupdateservicesd.SUCoreConfigScheduledScan +com.apple.softwareupdateservicesd.activity.autoDownload com.apple.softwareupdateservicesd.activity.autoDownloadEnd -com.apple.coreduet.client-needs-help.coreduetd -com.apple.fitness.FitnessAppInstalled -NewCarrierNotification -com.apple.duetexpertd.ATXAnchorModel.IdleTimeEndAnchor +com.apple.softwareupdateservicesd.activity.autoInstallEnd com.apple.softwareupdateservicesd.activity.autoInstallUnlock -com.apple.icloud.findmydeviced.localActivationLockInfoChanged -com.apple.calendar.database.preference.notification.kCalPreferredDaysToSyncKey -com.apple.accessibility.cache.enhance.text.legibility -com.apple.security.cloudkeychainproxy.kvstorechange3 -com.apple.MobileAsset.TimeZoneUpdate.ma.cached-metadata-updated +com.apple.softwareupdateservicesd.activity.autoScan com.apple.softwareupdateservicesd.activity.delayEndScan -com.apple.trial.NamespaceUpdate.FREEZER_POLICIES -com.apple.ams.provision-biometrics -com.apple.cddcommunicator.batteryChanged -com.apple.icloud.findmydeviced.findkit.magSafe.detach -com.apple.powermanagement.systempowerstate -com.apple.icloud.FindMy.addMagSafeAccessory -com.apple.GeoServices.navigation.stopped -kVMVoicemailTranscriptionTaskTranscribeAllVoicemails -com.apple.BiometricKit.passcodeGracePeriodChanged -INVoocabularyChangedNotification -com.apple.SynthesisProvider.updatedVoices -com.apple.security.tick -com.apple.security.secureobjectsync.circlechanged -com.apple.security.itembackup -com.apple.Preferences.ChangedRestrictionsEnabledStateNotification -com.apple.parsec-fbf.FLUploadImmediately -com.apple.cddcommunicator.thermalChanged -com.apple.system.thermalpressurelevel.cold -AppleNumberPreferencesChangedNotification -com.apple.kvs.store-did-change.com.apple.cloudsettings.sound -com.apple.stockholm.se.mfd -com.apple.security.secureobjectsync.viewschanged -SignificantTimeChangeNotification com.apple.softwareupdateservicesd.activity.emergencyAutoScan -com.apple.dataaccess.ping -com.apple.MobileSoftwareUpdate.OSVersionChanged -kCalBirthdayDefaultAlarmChangedNote -com.apple.system.timezone -com.apple.calendar.database.preference.notification.suggestEventLocations -com.apple.duetexpertd.ATXAnchorModel.invalidate.IdleTimeEndAnchor -com.apple.homehubd.endpointActivated +com.apple.softwareupdateservicesd.activity.installAlert +com.apple.softwareupdateservicesd.activity.presentBanner +com.apple.softwareupdateservicesd.activity.rollbackReboot +com.apple.softwareupdateservicesd.activity.splatAutoScan +com.apple.spotlight.SyndicatedContentDeleted +com.apple.spotlight.SyndicatedContentRefreshed +com.apple.spotlightui.prefschanged +com.apple.springboard.finishedstartup +com.apple.springboard.hasBlankedScreen +com.apple.springboard.lockstate +com.apple.springboard.pluggedin +com.apple.stockholm.se.mfd +com.apple.suggestions.prepareForQuery +com.apple.suggestions.settingsChanged +com.apple.symptoms.materialLinkQualityChange +com.apple.sysdiagnose.sysdiagnoseStarted +com.apple.system.clock_set +com.apple.system.config.network_change +com.apple.system.hostname +com.apple.system.logging.power_button_notification +com.apple.system.lowpowermode +com.apple.system.lowpowermode.auto_disabled +com.apple.system.lowpowermode.first_time +com.apple.system.powermanagement.poweradapter +com.apple.system.powermanagement.useractivity2 +com.apple.system.powermanagement.uservisiblepowerevent +com.apple.system.powersources.criticallevel com.apple.system.powersources.percent -com.apple.duetexpertd.appchangeprediction +com.apple.system.powersources.source +com.apple.system.powersources.timeremaining com.apple.system.thermalpressurelevel +com.apple.system.thermalpressurelevel.cold +com.apple.system.timezone +com.apple.tcc.access.changed +com.apple.telephonyutilities.callservicesd.fakeincomingmessage +com.apple.telephonyutilities.callservicesd.fakeoutgoingmessage +com.apple.telephonyutilities.callservicesdaemon.voicemailcallended +com.apple.thermalmonitor.ageAwareMitigationsEnabled com.apple.timezonesync.idslaunchnotification -com.apple.MobileAsset.VoiceTriggerHSAssetsWatch.ma.new-asset-installed -com.apple.mobilecal.preference.notification.calendarsExcludedFromNotifications -com.apple.system.powersources.source -com.apple.duetexpertd.ATXMMAppPredictor.BluetoothDisconnectedAnchor -com.apple.reminder.list.name.siri_data_changed -com.apple.managedconfiguration.allowpasscodemodificationchanged -com.apple.nanoregistry.devicedidunpair -com.apple.homed.user-cloud-share.wake.com.apple.mediaservicesbroker.container -com.apple.MobileAsset.VoiceServices.GryphonVoice.ma.new-asset-installed -com.apple.awdd.anonymity -com.apple.appstored.iCloudSubEntitlementsCacheUpdated -SLSharedWithYouAppSettingHasChanged -com.apple.nanoregistry.paireddevicedidchangecapabilities -com.apple.SensorKit.phoneUsageReport -com.apple.LaunchServices.applicationRegistered -com.apple.AirTunes.DACP.repeatadv -com.apple.audio.AOP.enable -com.apple.locationd/Prefs -security.mac.amfi.developer_mode_status.changed -com.apple.MobileAsset.SpeechEndpointAssets.cached-metadata-updated -com.apple.siri.inference.biome-context -com.apple.sbd.kvstorechange -com.apple.system.powermanagement.useractivity2 -com.apple.coreaudio.speechDetectionVAD.created -com.apple.ProtectedCloudStorage.rollIfAged -com.apple.MediaRemote.nowPlayingInfoDidChange -com.apple.locationd.authorization -com.apple.GeoServices.navigation.started -com.apple.MobileAsset.SecureElementServiceAssets.ma.new-asset-installed -com.apple.softwareupdateservicesd.activity.autoScan -com.apple.kvs.store-did-change.com.apple.cloudsettings.trackpad -com.apple.purplebuddy.setupexited -com.apple.remindd.nano_preferences_sync -com.apple.voicetrigger.RemoteDarwin.EarlyDetect -com.apple.itunesstored.invalidatebags -com.apple.EscrowSecurityAlert.record -com.apple.ContinuityKeyBoard.enabled -com.apple.iokit.hid.displayStatus -com.apple.system.powermanagement.uservisiblepowerevent -com.apple.nanoregistry.watchdidbecomeactive -com.apple.softwareupdateservicesd.activity.rollbackReboot -FMFFollowersChangedNotification -FMFMeDeviceChangedNotification -com.apple.MobileAsset.SecureElementServiceAssets.ma.cached-metadata-updated -com.apple.duetexpertd.ATXAnchorModel.BluetoothConnectedAnchor -com.apple.security.secureobjectsync.holdlock -com.apple.pex.connections.focalappchanged -com.apple.Sharing.prefsChanged -com.apple.system.hostname -com.apple.appstored.PodcastSubEntitlementsCacheUpdated -com.apple.kvs.store-did-change.com.apple.cloudsettings.gamecontroller -com.apple.MobileAsset.VoiceTriggerAssetsWatch.ma.new-asset-installed -com.apple.callhistorysync.idslaunchnotification -com.apple.icloud.findmydeviced.findkit.magSafe.attach -SPSpotlightRecentsCacheDidChange -com.apple.screensharing.idslaunchnotification -com.apple.mobileme.fmf1.allowFindMyFriendsModification -com.apple.chatkit.groups.siri_data_changed -com.apple.GeoServices.countryCodeChanged -com.apple.MobileAsset.VoiceTriggerAssetsWatch.ma.cached-metadata-updated -com.apple.remotemanagement.accountsChanged -com.apple.duetexpertd.defaultsChanged -com.apple.AirTunes.DACP.devicevolumechanged -com.apple.accessibility.cache.differentiate.without.color -com.apple.mobiletimerd.diagnostics -com.apple.ProtectedCloudStorage.rollNow -com.apple.sleepd.diagnostics com.apple.touchsetupd.launch -com.apple.trial.NamespaceUpdate.SIRI_DICTATION_ASSETS -com.apple.cloudd.pcsIdentityUpdate-com.apple.ProactivePredictionsBackup +com.apple.trial.NamespaceUpdate.FREEZER_POLICIES com.apple.trial.NamespaceUpdate.NETWORK_SERVICE_PROXY_CONFIG_UPDATE -BYSetupAssistantFinishedDarwinNotification -com.apple.siri.client.state.DynamiteClientState.siri_data_changed -com.apple.Preferences.ResetPrivacyWarningsNotification -kFaceTimeChangedNotification -FMFDevicesChangedNotification -VVMessageWaitingFallbackNotification -CKAccountChangedNotification -com.apple.MobileAsset.VoiceTriggerAssetsMarsh.ma.cached-metadata-updated -com.apple.coreduetd.knowledgebase.launch.duetexpertd -com.apple.MobileBackup.backgroundCellularAccessChanged -com.apple.homed.user-cloud-share.repair.wake.com.apple.applemediaservices.multiuser -com.apple.suggestions.prepareForQuery -AFLanguageCodeDidChangeDarwinNotification -com.apple.networkextension.nehelper-init -com.apple.GeoServices.PreferencesSync.SettingsChanged -com.apple.homed.speakersConfiguredChanged -ApplePreferredContentSizeCategoryChangedNotification -dmf.policy.monitor.app -com.apple.voicetrigger.PHSProfileModified -com.apple.duet.expertcenter.appRefresh -com.apple.duetexpertd.ATXAnchorModel.CarPlayConnectedAnchor -com.apple.icloudpairing.idslaunchnotification -com.apple.accessories.connection.passedMFi4Auth -com.apple.bluetooth.daemonStarted -CNFavoritesChangedExternallyNotification -VMStoreSetTokenNotification -com.apple.spotlight.SyndicatedContentDeleted -com.apple.coreduet.idslaunchnotification -com.apple.appstored.MusicSubEntitlementsCacheUpdated +com.apple.trial.NamespaceUpdate.SIRI_DICTATION_ASSETS com.apple.trial.NamespaceUpdate.SIRI_TEXT_TO_SPEECH -com.apple.MobileAsset.VoiceTriggerHSAssets.ma.new-asset-installed -com.apple.CloudSubscriptionFeature.Changed -com.apple.bluetooth.pairing -com.apple.telephonyutilities.callservicesd.fakeincomingmessage -com.apple.mobile.disk_image_mounted -com.apple.kvs.store-did-change.com.apple.cloudsettings.desktop -com.apple.sleep.sync.SleepSettingsDidChange -com.apple.nfcacd.multitag.state.change -com.apple.VideosUI.StoreAcquisitionCrossProcessNotification -com.apple.duetexpertd.prefschanged -com.apple.kvs.store-did-change.com.apple.cloudsettings.general -com.apple.LaunchServices.applicationUnregistered -com.apple.AppleMediaServices.deviceOffersChanged \ No newline at end of file +com.apple.trial.NamespaceUpdate.SIRI_UNDERSTANDING_ASR_ASSISTANT +com.apple.trial.NamespaceUpdate.SIRI_UNDERSTANDING_ATTENTION_ASSETS +com.apple.trial.NamespaceUpdate.SIRI_UNDERSTANDING_NL +com.apple.trial.NamespaceUpdate.SIRI_UNDERSTANDING_NL_OVERRIDES +com.apple.trial.bmlt.activated +com.apple.triald.new-experiment +com.apple.triald.wake +com.apple.ttsasset.NewAssetNotification +com.apple.tv.TVWidgetExtension.Register +com.apple.tv.appRemoved +com.apple.tv.updateAppVisibility +com.apple.videos.migrationCompleted +com.apple.voicemail.ReloadService +com.apple.voicemail.VVVerifierCheckpointDictionaryChanged +com.apple.voicemail.changed +com.apple.voiceservices.notification.voice-update +com.apple.voiceservices.trigger.asset-force-update +com.apple.voicetrigger.EarlyDetect +com.apple.voicetrigger.PHSProfileModified +com.apple.voicetrigger.RemoteDarwin.ConnectionChanged +com.apple.voicetrigger.RemoteDarwin.EarlyDetect +com.apple.voicetrigger.XPCRestarted +com.apple.voicetrigger.enablePolicyChanged +com.apple.wcd.wake-up +com.apple.welcomekitinternalsettings.dismissed +com.apple.wirelessinsightsd.anonymity +com.apple.wirelessproximity.launch +dmf.policy.monitor.app +kAFPreferencesDidChangeDarwinNotification +kCalBirthdayDefaultAlarmChangedNote +kCalEventOccurrenceCacheChangedNotification +kFZACAppBundleIdentifierLaunchNotification +kFZVCAppBundleIdentifierLaunchNotification +kFaceTimeChangedNotification +kKeepAppsUpToDateEnabledChangedNotification +kVMVoicemailTranscriptionTaskTranscribeAllVoicemails +logging tasks have changed +security.mac.amfi.developer_mode_status.changed \ No newline at end of file From a4514169048cab4929ad3efd44e408f3cbefd364 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 15 Aug 2023 08:17:37 +0300 Subject: [PATCH 171/234] remote: ignore import errors only on windows (#527) --- pymobiledevice3/cli/remote.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 8b8d992e3..68d24b343 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -1,5 +1,6 @@ import asyncio import logging +import platform from typing import List, TextIO import click @@ -14,7 +15,8 @@ from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service except ImportError: # isn't supported on Windows - pass + if platform.system() != 'Windows': + raise logger = logging.getLogger(__name__) From 8cc0888ebe67e927cf7db17af6abafc15bdf318d Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 15 Aug 2023 08:40:05 +0300 Subject: [PATCH 172/234] remote: prompt the user to solve `ImportError` --- pymobiledevice3/cli/remote.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 68d24b343..7d3e6e009 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -1,6 +1,5 @@ import asyncio import logging -import platform from typing import List, TextIO import click @@ -11,14 +10,15 @@ from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService from pymobiledevice3.remote.utils import stop_remoted +logger = logging.getLogger(__name__) + try: from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service except ImportError: - # isn't supported on Windows - if platform.system() != 'Windows': - raise - -logger = logging.getLogger(__name__) + logger.warning( + 'create_core_device_tunnel_service failed to be imported. Some feature may not work.\n' + 'You can debug this by trying the import yourself:\n\n' + 'from pymobiledevice3.remote.core_device_tunnel_service import create_core_device_tunnel_service') def get_device_list() -> List[RemoteServiceDiscoveryService]: From aa1ec0078e15820c543b2341241c7f3125f06d6e Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 15 Aug 2023 08:49:42 +0300 Subject: [PATCH 173/234] README: add notice for `openssl` requirement --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e146bc07e..5c56a5645 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - [News](#news) - [Description](#description) - [Installation](#installation) - * [Lower iOS versions (<13)](#lower-ios-versions-13) + * [OpenSSL libraries](#openssl-libraries) - [Usage](#usage) * [Python API](#python-api) * [Working with developer tools (iOS >= 17.0)](#working-with-developer-tools-ios--170) @@ -82,19 +82,24 @@ eval "$(_PYMOBILEDEVICE3_COMPLETE=source_zsh pymobiledevice3)" eval "$(_PYMOBILEDEVICE3_COMPLETE=zsh_source pymobiledevice3)" ``` -## Lower iOS versions (<13) +## OpenSSL libraries -If you wish to use pymobiledevice3 with iOS versions lower than 13, Make sure to install `openssl`: +Currently, openssl is explicitly required if using on older iOS version (<13) or creating a QUIC tunnel (iOS>=17). -On MAC: +On macOS: ```shell +# for iOS>=17 support +brew install openssl@3 + +# for iOS<13 support brew install openssl ``` On Linux: ```shell +# for iOS<13 support sudo apt install openssl ``` From 6d28631437b6c5e66231289d2b6a57d051aa3a69 Mon Sep 17 00:00:00 2001 From: Matan Perelman Date: Tue, 15 Aug 2023 11:22:36 +0300 Subject: [PATCH 174/234] core_profile_session_tap: Fix taskgroup decoding in stackshot --- .../services/dvt/instruments/core_profile_session_tap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py b/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py index 5a71ade61..a12870543 100644 --- a/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +++ b/pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py @@ -233,10 +233,11 @@ thread_group_snapshot_trace_v3 = Struct( 'tgs_id' / Int64ul, - '_tgs_name' / FixedSized(16, GreedyString('utf8')), + '_tgs_name' / Bytes(16), 'tgs_flags' / Int64ul, - '_tgs_name_cont' / FixedSized(16, GreedyString('utf8')), - 'tgs_name' / Computed(lambda ctx: ctx._tgs_name.strip('\x00') + ctx._tgs_name_cont.strip('\x00')), + '_tgs_name_cont' / Bytes(16), + 'tgs_name' / Computed( + lambda ctx: (ctx._tgs_name.strip(b'\x00') + ctx._tgs_name_cont.strip(b'\x00')).decode('utf-8')), ) thread_group_snapshot = Struct( From 11b2ec1d5982608f339ecdca30b18d603b3fbb15 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 15 Aug 2023 19:05:56 +0300 Subject: [PATCH 175/234] cli: add details about the tunneled device --- pymobiledevice3/cli/remote.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 7d3e6e009..009dd53e7 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -71,6 +71,12 @@ async def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, sec if secrets is not None: print(click.style('Secrets: ', bold=True, fg='magenta') + click.style(secrets.name, bold=True, fg='white')) + print(click.style('UDID: ', bold=True, fg='yellow') + + click.style(service_provider.udid, bold=True, fg='white')) + print(click.style('ProductType: ', bold=True, fg='yellow') + + click.style(service_provider.product_type, bold=True, fg='white')) + print(click.style('ProductVersion: ', bold=True, fg='yellow') + + click.style(service_provider.product_version, bold=True, fg='white')) print(click.style('Interface: ', bold=True, fg='yellow') + click.style(tunnel_result.interface, bold=True, fg='white')) print(click.style('RSD Address: ', bold=True, fg='yellow') + From 8e2eb9cced0eba61a746a80da549bcf9116a4b76 Mon Sep 17 00:00:00 2001 From: Matan Perelman Date: Wed, 16 Aug 2023 08:38:38 +0300 Subject: [PATCH 176/234] cli_common: Fix service_provider name clash --- pymobiledevice3/cli/cli_common.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/cli/cli_common.py b/pymobiledevice3/cli/cli_common.py index db40ae056..60a2bb980 100644 --- a/pymobiledevice3/cli/cli_common.py +++ b/pymobiledevice3/cli/cli_common.py @@ -3,7 +3,7 @@ import logging import os import uuid -from typing import List, Optional, Tuple +from typing import Callable, List, Mapping, Optional, Tuple import click import coloredlogs @@ -66,6 +66,20 @@ def prompt_device_list(device_list: List): raise NoDeviceSelectedError() +def choose_service_provider(callback: Callable): + def wrap_callback_calling(**kwargs: Mapping): + service_provider = None + lockdown_service_provider = kwargs.pop('lockdown_service_provider', None) + rsd_service_provider = kwargs.pop('rsd_service_provider', None) + if lockdown_service_provider is not None: + service_provider = lockdown_service_provider + if rsd_service_provider is not None: + service_provider = rsd_service_provider + callback(service_provider=service_provider, **kwargs) + + return wrap_callback_calling + + class BaseCommand(click.Command): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -73,13 +87,14 @@ def __init__(self, *args, **kwargs): click.Option(('verbosity', '-v', '--verbose'), count=True, callback=set_verbosity, expose_value=False), ] self.service_provider = None + self.callback = choose_service_provider(self.callback) class LockdownCommand(BaseCommand): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.params[:0] = [ - click.Option(('service_provider', '--udid'), envvar=UDID_ENV_VAR, callback=self.udid, + click.Option(('lockdown_service_provider', '--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'), ] @@ -106,7 +121,7 @@ class RSDCommand(BaseCommand): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.params[:0] = [ - click.Option(('service_provider', '--rsd'), type=(str, int), callback=self.rsd, required=True, + click.Option(('rsd_service_provider', '--rsd'), type=(str, int), callback=self.rsd, required=True, help='RSD hostname and port number'), ] From d00af4533234c3100a855535c5a38858a4117607 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 16 Aug 2023 08:45:40 +0300 Subject: [PATCH 177/234] cli: add `--udid` to `start-quic-tunnel` --- pymobiledevice3/cli/remote.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 009dd53e7..b256c68dd 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -6,6 +6,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa from pymobiledevice3.cli.cli_common import RSDCommand, print_json, prompt_device_list +from pymobiledevice3.exceptions import NoDeviceConnectedError from pymobiledevice3.remote.bonjour import get_remoted_addresses from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService from pymobiledevice3.remote.utils import stop_remoted @@ -92,17 +93,32 @@ async def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, sec @remote_cli.command('start-quic-tunnel') +@click.option('--udid', help='UDID for a specific device to look for') @click.option('--secrets', type=click.File('wt'), help='TLS keyfile for decrypting with Wireshark') -def cli_start_quic_tunnel(secrets: TextIO): +def cli_start_quic_tunnel(udid: str, secrets: TextIO): """ start quic tunnel """ devices = get_device_list() if not devices: - print('No device could be found') - return + # no devices were found + raise NoDeviceConnectedError() if len(devices) == 1: + # only one device found rsd = devices[0] else: - rsd = prompt_device_list(devices) + # several devices were found + if udid is not None: + # show prompt if non explicitly selected + rsd = prompt_device_list(devices) + else: + rsd = [device for device in devices if device.udid == udid] + if len(rsd) > 0: + rsd = rsd[0] + else: + raise NoDeviceConnectedError() + + if udid is not None and rsd.udid != udid: + raise NoDeviceConnectedError() + asyncio.run(start_quic_tunnel(rsd, secrets), debug=True) From 31d981ef0c55d69095b2d72b201e27837f9fe783 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 16 Aug 2023 09:04:11 +0300 Subject: [PATCH 178/234] pyproject: bump version to 2.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 18ea49b91..89f6405b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.3.1" +version = "2.4.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 4c0fb45040d5a74cdcf72e09a22afb71d456a80e Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 17 Aug 2023 14:36:20 +0300 Subject: [PATCH 179/234] cli: add `lockdown developer-service` --- pymobiledevice3/cli/lockdown.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/cli/lockdown.py b/pymobiledevice3/cli/lockdown.py index dd0382384..f96a6b7ba 100644 --- a/pymobiledevice3/cli/lockdown.py +++ b/pymobiledevice3/cli/lockdown.py @@ -5,6 +5,7 @@ from pymobiledevice3.cli.cli_common import Command, CommandWithoutAutopair, print_json from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.services.heartbeat import HeartbeatService logger = logging.getLogger(__name__) @@ -30,11 +31,18 @@ def lockdown_recovery(service_provider: LockdownClient): @lockdown_group.command('service', cls=Command) @click.argument('service_name') -def lockdown_service(service_provider: LockdownClient, service_name): - """ send-receive raw service messages """ +def lockdown_service(service_provider: LockdownServiceProvider, service_name): + """ send-receive raw service messages with a given service name""" service_provider.start_lockdown_service(service_name).shell() +@lockdown_group.command('developer-service', cls=Command) +@click.argument('service_name') +def lockdown_developer_service(service_provider: LockdownServiceProvider, service_name): + """ send-receive raw service messages with a given developer service name """ + service_provider.start_lockdown_developer_service(service_name).shell() + + @lockdown_group.command('info', cls=Command) @click.option('-a', '--all', is_flag=True, help='include all domain information') @click.option('--color/--no-color', default=True) From d22b7d136111227ac2dcc085c2cb4923b635a36b Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 17 Aug 2023 14:37:23 +0300 Subject: [PATCH 180/234] cli: refactor `remote shell` -> `remote service` --- pymobiledevice3/cli/remote.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index b256c68dd..aaa5d40ab 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -122,9 +122,9 @@ def cli_start_quic_tunnel(udid: str, secrets: TextIO): asyncio.run(start_quic_tunnel(rsd, secrets), debug=True) -@remote_cli.command('shell', cls=RSDCommand) -@click.argument('service') -def cli_shell(service_provider: RemoteServiceDiscoveryService, service: str): +@remote_cli.command('service', cls=RSDCommand) +@click.argument('service_name') +def cli_service(service_provider: RemoteServiceDiscoveryService, service_name: str): """ start an ipython shell for interacting with given service """ - with service_provider.start_remote_service(service) as service: + with service_provider.start_remote_service(service_name) as service: service.shell() From e4f13c0a604c99a88f839a62ac1a91bb25a26c30 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 17 Aug 2023 18:18:08 +0300 Subject: [PATCH 181/234] remotexpc_sniffer: make every packet flush --- misc/remotexpc_sniffer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misc/remotexpc_sniffer.py b/misc/remotexpc_sniffer.py index a49f4b1b7..5707736d8 100644 --- a/misc/remotexpc_sniffer.py +++ b/misc/remotexpc_sniffer.py @@ -200,8 +200,7 @@ def offline(file: str): def live(iface: str): """ Parse RemoteXPC live from a given network interface """ sniffer = RemoteXPCSniffer() - for p in sniff(iface=iface): - sniffer.process_packet(p) + sniff(iface=iface, prn=sniffer.process_packet) if __name__ == '__main__': From fa15a108ccbf8af3e62f012a0a9ef1ac466db4bc Mon Sep 17 00:00:00 2001 From: DoronZ Date: Fri, 18 Aug 2023 12:56:24 +0300 Subject: [PATCH 182/234] cli: make `dvt simulate-location` wait user input --- pymobiledevice3/cli/developer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 78646e1f9..cb4c6c9ee 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -945,6 +945,7 @@ def dvt_simulate_location_set(service_provider: LockdownClient, latitude, longit """ with DvtSecureSocketProxyService(service_provider) as dvt: LocationSimulation(dvt).simulate_location(latitude, longitude) + wait_return() @developer.group() From bbfbf761a785c0d9f3e2b8aaa029a04fb241dbd7 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Sat, 19 Aug 2023 01:41:45 +0300 Subject: [PATCH 183/234] pyproject: bump version to 2.5.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 89f6405b8..14cd5921b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.4.0" +version = "2.5.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 929608481528770c8adf8ff0559aea7af4e44016 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 14:25:25 -0500 Subject: [PATCH 184/234] [CLI] restore: Look for device connected via USB only --- pymobiledevice3/cli/restore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/restore.py b/pymobiledevice3/cli/restore.py index 6d962ffdd..cb306b220 100644 --- a/pymobiledevice3/cli/restore.py +++ b/pymobiledevice3/cli/restore.py @@ -46,7 +46,7 @@ def device(ctx, param, value): logger.debug('searching among connected devices via lockdownd') for device in usbmux.list_devices(): try: - lockdown = create_using_usbmux(serial=device.serial) + lockdown = create_using_usbmux(serial=device.serial, connection_type='USB') except IncorrectModeError: continue if (ecid is None) or (lockdown.ecid == value): From 802037ce5c90d35560f2b4c57a97a5093d563a68 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 14:28:09 -0500 Subject: [PATCH 185/234] [restored_client] Only search for devices connected via USB --- pymobiledevice3/restore/restored_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/restore/restored_client.py b/pymobiledevice3/restore/restored_client.py index fcd4491d5..21262c147 100644 --- a/pymobiledevice3/restore/restored_client.py +++ b/pymobiledevice3/restore/restored_client.py @@ -14,7 +14,7 @@ class RestoredClient(object): def __init__(self, udid=None, client_name=DEFAULT_CLIENT_NAME): self.logger = logging.getLogger(__name__) self.udid = self._get_or_verify_udid(udid) - self.service = LockdownServiceConnection.create_using_usbmux(self.udid, self.SERVICE_PORT) + self.service = LockdownServiceConnection.create_using_usbmux(self.udid, self.SERVICE_PORT, connection_type='USB') self.label = client_name self.query_type = self.service.send_recv_plist({'Request': 'QueryType'}) self.version = self.query_type.get('RestoreProtocolVersion') From 14f510425beca8fc443cab509fb335739157552a Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 15:39:09 -0500 Subject: [PATCH 186/234] [Restore] fIx the rest of the create_using_usbmux() calls --- pymobiledevice3/restore/asr.py | 2 +- pymobiledevice3/restore/fdr.py | 4 ++-- pymobiledevice3/restore/recovery.py | 2 +- pymobiledevice3/restore/restore.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pymobiledevice3/restore/asr.py b/pymobiledevice3/restore/asr.py index 984106671..ca5e651bd 100644 --- a/pymobiledevice3/restore/asr.py +++ b/pymobiledevice3/restore/asr.py @@ -29,7 +29,7 @@ class ASRClient(object): SERVICE_PORT = ASR_PORT def __init__(self, udid: str): - self.service = LockdownServiceConnection.create_using_usbmux(udid, self.SERVICE_PORT) + self.service = LockdownServiceConnection.create_using_usbmux(udid, self.SERVICE_PORT, connection_type='USB') # receive Initiate command message data = self.recv_plist() diff --git a/pymobiledevice3/restore/fdr.py b/pymobiledevice3/restore/fdr.py index 2b0a20e37..243bcb1a2 100644 --- a/pymobiledevice3/restore/fdr.py +++ b/pymobiledevice3/restore/fdr.py @@ -48,10 +48,10 @@ def __init__(self, type_: fdr_type, udid=None): logger.debug('connecting to FDR') if type_ == fdr_type.FDR_CTRL: - self.service = LockdownServiceConnection.create_using_usbmux(device.serial, self.SERVICE_PORT) + self.service = LockdownServiceConnection.create_using_usbmux(device.serial, self.SERVICE_PORT, connection_type='USB') self.ctrl_handshake() else: - self.service = LockdownServiceConnection.create_using_usbmux(device.serial, conn_port) + self.service = LockdownServiceConnection.create_using_usbmux(device.serial, conn_port, connection_type='USB') self.sync_handshake() logger.debug('FDR connected') diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index a83ab5424..5ffe80190 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -454,7 +454,7 @@ def boot_ramdisk(self): self.logger.info('going into Recovery') # in case lockdown has disconnected while waiting for a ticket - self.device.lockdown = create_using_usbmux(serial=self.device.lockdown.udid) + self.device.lockdown = create_using_usbmux(serial=self.device.lockdown.udid, connection_type='USB') self.device.lockdown.enter_recovery() self.device.lockdown = None diff --git a/pymobiledevice3/restore/restore.py b/pymobiledevice3/restore/restore.py index 44925a758..347e82a3d 100644 --- a/pymobiledevice3/restore/restore.py +++ b/pymobiledevice3/restore/restore.py @@ -633,7 +633,7 @@ def send_bootability_bundle_data(self, message): while True: try: - client = LockdownServiceConnection.create_using_usbmux(self._restored.udid, data_port) + client = LockdownServiceConnection.create_using_usbmux(self._restored.udid, data_port, connection_type='USB') break except ConnectionFailedError: self.logger.debug('Retrying connection...') @@ -1153,7 +1153,7 @@ def handle_baseband_updater_output_data(self, message: Mapping): while True: try: - client = LockdownServiceConnection.create_using_usbmux(self._restored.udid, data_port) + client = LockdownServiceConnection.create_using_usbmux(self._restored.udid, data_port, connection_type='USB') break except ConnectionFailedError: self.logger.debug('Retrying connection...') @@ -1198,7 +1198,7 @@ def restore_device(self): if self._ignore_fdr: self.logger.info('Establishing a mock FDR listener') - self._fdr = LockdownServiceConnection.create_using_usbmux(self._restored.udid, FDRClient.SERVICE_PORT) + self._fdr = LockdownServiceConnection.create_using_usbmux(self._restored.udid, FDRClient.SERVICE_PORT, connection_type='USB') else: self.logger.info('Starting FDR listener thread') start_fdr_thread(fdr_type.FDR_CTRL) From 7edf98e0cbbb9000a4aca1f62cef49219da10c89 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 22:26:25 -0500 Subject: [PATCH 187/234] [IRecv] reset: Add notice to disconnect/reconnect device if hang --- pymobiledevice3/irecv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymobiledevice3/irecv.py b/pymobiledevice3/irecv.py index 163fea6ba..216c1ea7c 100644 --- a/pymobiledevice3/irecv.py +++ b/pymobiledevice3/irecv.py @@ -207,6 +207,7 @@ def send_buffer(self, buf: bytes): def reset(self): try: logger.debug('resetting usb device') + logger.info('If the restore hangs here, disconnect & reconnect the device') self._device.reset() except USBError: pass From d338eab171170395dee1a2c08a46eefc16dce9fb Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 22:28:38 -0500 Subject: [PATCH 188/234] [Recovery] dfu_enter_recovery: Fix some stuff - Don't send unnecessary commands & requests on pre-A10 - Ignore an error --- pymobiledevice3/restore/recovery.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index 5ffe80190..4fd4148b6 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -390,7 +390,11 @@ def enter_restore(self): def dfu_enter_recovery(self): self.send_component('iBSS') - self.reconnect_irecv() + try: + self.reconnect_irecv() + except: + self.reconnect_irecv(is_recovery=True) + if 'SRTG' in self.device.irecv._device_info: raise PyMobileDevice3Exception('Device failed to enter recovery') @@ -416,18 +420,22 @@ def dfu_enter_recovery(self): self.device.irecv.send_command('setenvnp boot-args rd=md0 nand-enable-reformat=1 -progress -restore') self.send_applelogo(allow_missing=False) + mode = self.device.irecv.mode # send iBEC self.send_component('iBEC') - if self.device.irecv and self.device.irecv.mode.is_recovery: + if self.device.irecv and mode.is_recovery: time.sleep(1) self.device.irecv.send_command('go', b_request=1) if self.build_identity.build_manifest.build_major < 20: - self.device.irecv.ctrl_transfer(0x21, 1, timeout=5000) + try: + self.device.irecv.ctrl_transfer(0x21, 1, timeout=5000) + except USBError: + pass - self.logger.debug('Waiting for device to disconnect...') - time.sleep(10) + self.logger.debug('Waiting for device to disconnect...') + time.sleep(10) self.logger.debug('Waiting for device to reconnect in recovery mode...') self.reconnect_irecv(is_recovery=True) From 4adf4e726103f3c551fbf16b3291d4a0988b671f Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 22:48:01 -0500 Subject: [PATCH 189/234] [Recovery] boot_ramdisk: Clean up logic --- pymobiledevice3/restore/recovery.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index 4fd4148b6..1119ec248 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -445,19 +445,7 @@ def boot_ramdisk(self): self.logger.info('fetching TSS record') self.fetch_tss_record() - if self.device.irecv: - if self.device.irecv.mode == Mode.DFU_MODE: - # device is currently in DFU mode, place it into recovery mode - self.dfu_enter_recovery() - elif self.device.irecv.mode.is_recovery: - # now we load the iBEC - try: - self.send_ibec() - except USBError: - pass - - self.reconnect_irecv() - elif self.device.lockdown: + if self.device.lockdown: # normal mode self.logger.info('going into Recovery') @@ -469,13 +457,18 @@ def boot_ramdisk(self): self.device.irecv = IRecv(self.device.ecid) self.reconnect_irecv() + if self.device.irecv.mode == Mode.DFU_MODE: + # device is currently in DFU mode, place it into recovery mode + self.dfu_enter_recovery() + + elif self.device.irecv.mode.is_recovery: # now we load the iBEC try: self.send_ibec() except USBError: pass - self.reconnect_irecv() + self.reconnect_irecv(is_recovery=True) self.logger.info('device booted into recovery') From 4946a7a51d25bc36bc8e73ac21d1f1e7d01af1c9 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 22:54:11 -0500 Subject: [PATCH 190/234] [Recovery] dfu_enter_recovery: Don't think this ended up being necessary... --- pymobiledevice3/restore/recovery.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pymobiledevice3/restore/recovery.py b/pymobiledevice3/restore/recovery.py index 1119ec248..c36d82c73 100644 --- a/pymobiledevice3/restore/recovery.py +++ b/pymobiledevice3/restore/recovery.py @@ -390,10 +390,7 @@ def enter_restore(self): def dfu_enter_recovery(self): self.send_component('iBSS') - try: - self.reconnect_irecv() - except: - self.reconnect_irecv(is_recovery=True) + self.reconnect_irecv() if 'SRTG' in self.device.irecv._device_info: raise PyMobileDevice3Exception('Device failed to enter recovery') From 1e7e3a79ee27dde0a0dd791b14c227f9ac199879 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Sat, 19 Aug 2023 22:58:08 -0500 Subject: [PATCH 191/234] Appease the flake8 gods... --- pymobiledevice3/restore/fdr.py | 4 +++- pymobiledevice3/restore/restore.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/restore/fdr.py b/pymobiledevice3/restore/fdr.py index 243bcb1a2..806905a25 100644 --- a/pymobiledevice3/restore/fdr.py +++ b/pymobiledevice3/restore/fdr.py @@ -48,7 +48,9 @@ def __init__(self, type_: fdr_type, udid=None): logger.debug('connecting to FDR') if type_ == fdr_type.FDR_CTRL: - self.service = LockdownServiceConnection.create_using_usbmux(device.serial, self.SERVICE_PORT, connection_type='USB') + self.service = LockdownServiceConnection.create_using_usbmux( + device.serial, self.SERVICE_PORT, connection_type='USB' + ) self.ctrl_handshake() else: self.service = LockdownServiceConnection.create_using_usbmux(device.serial, conn_port, connection_type='USB') diff --git a/pymobiledevice3/restore/restore.py b/pymobiledevice3/restore/restore.py index 347e82a3d..88176e58b 100644 --- a/pymobiledevice3/restore/restore.py +++ b/pymobiledevice3/restore/restore.py @@ -1198,7 +1198,9 @@ def restore_device(self): if self._ignore_fdr: self.logger.info('Establishing a mock FDR listener') - self._fdr = LockdownServiceConnection.create_using_usbmux(self._restored.udid, FDRClient.SERVICE_PORT, connection_type='USB') + self._fdr = LockdownServiceConnection.create_using_usbmux( + self._restored.udid, FDRClient.SERVICE_PORT, connection_type='USB' + ) else: self.logger.info('Starting FDR listener thread') start_fdr_thread(fdr_type.FDR_CTRL) From 29690244d7ae2d8f368b662d1e7a5f986800e1af Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 20 Aug 2023 14:00:43 +0300 Subject: [PATCH 192/234] pyproject: bump version to 2.5.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 14cd5921b..7b43b8f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.5.0" +version = "2.5.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 2b14a367e964d9def393d1df6e788152573770b9 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 22 Aug 2023 08:52:44 +0300 Subject: [PATCH 193/234] tcp_forwarder: refactor to support rsd --- pymobiledevice3/cli/developer.py | 12 ++-- pymobiledevice3/cli/usbmux.py | 4 +- pymobiledevice3/tcp_forwarder.py | 90 +++++++++++++++++++--------- tests/services/test_tcp_forwarder.py | 4 +- 4 files changed, 73 insertions(+), 37 deletions(-) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index cb4c6c9ee..38a822ff5 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -49,7 +49,7 @@ from pymobiledevice3.services.remote_server import RemoteServer from pymobiledevice3.services.screenshot import ScreenshotService from pymobiledevice3.services.simulate_location import DtSimulateLocation -from pymobiledevice3.tcp_forwarder import TcpForwarder +from pymobiledevice3.tcp_forwarder import LockdownTcpForwarder BSC_SUBCLASS = 0x40c BSC_CLASS = 0x4 @@ -872,9 +872,13 @@ def debugserver_start_server(service_provider: LockdownClient, local_port): (lldb) platform connect connect://localhost: """ - attr = service_provider.get_service_connection_attributes('com.apple.debugserver.DVTSecureSocketProxy') - TcpForwarder(local_port, attr['Port'], serial=service_provider.identifier, - enable_ssl=attr.get('EnableServiceSSL', False)).start() + + if Version(service_provider.product_version) < Version('17.0'): + service_name = 'com.apple.debugserver.DVTSecureSocketProxy' + else: + service_name = 'com.apple.internal.dt.remote.debugproxy' + + LockdownTcpForwarder(service_provider, local_port, service_name).start() @developer.group('arbitration') diff --git a/pymobiledevice3/cli/usbmux.py b/pymobiledevice3/cli/usbmux.py index 11a680325..c27b708aa 100644 --- a/pymobiledevice3/cli/usbmux.py +++ b/pymobiledevice3/cli/usbmux.py @@ -6,7 +6,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.cli.cli_common import print_json from pymobiledevice3.lockdown import create_using_usbmux -from pymobiledevice3.tcp_forwarder import TcpForwarder +from pymobiledevice3.tcp_forwarder import UsbmuxTcpForwarder logger = logging.getLogger(__name__) @@ -30,7 +30,7 @@ def usbmux_cli(): @click.option('-d', '--daemonize', is_flag=True) def usbmux_forward(src_port: int, dst_port: int, serial: str, daemonize: bool): """ forward tcp port """ - forwarder = TcpForwarder(src_port, dst_port, serial=serial) + forwarder = UsbmuxTcpForwarder(serial, src_port, dst_port) if daemonize: try: diff --git a/pymobiledevice3/tcp_forwarder.py b/pymobiledevice3/tcp_forwarder.py index ca1b2d5a7..f40bccb15 100644 --- a/pymobiledevice3/tcp_forwarder.py +++ b/pymobiledevice3/tcp_forwarder.py @@ -2,14 +2,14 @@ import select import socket import threading +from abc import abstractmethod from pymobiledevice3 import usbmux from pymobiledevice3.exceptions import ConnectionFailedError -from pymobiledevice3.lockdown import create_using_usbmux -from pymobiledevice3.service_connection import LockdownServiceConnection +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider -class TcpForwarder: +class TcpForwarderBase: """ Allows forwarding local tcp connection into the device via a given lockdown connection """ @@ -17,28 +17,20 @@ class TcpForwarder: MAX_FORWARDED_CONNECTIONS = 200 TIMEOUT = 1 - def __init__(self, src_port: int, dst_port: int, serial: str = None, enable_ssl=False, - listening_event: threading.Event = None, usbmux_connection_type: str = None): + def __init__(self, src_port: int, listening_event: threading.Event = None): """ Initialize a new tcp forwarder :param src_port: tcp port to listen on - :param dst_port: tcp port to connect to each new connection via the supplied lockdown object - :param serial: device serial :param enable_ssl: enable ssl wrapping for the transferred data :param listening_event: event to fire when the listening occurred - :param usbmux_connection_type: preferred connection type """ self.logger = logging.getLogger(__name__) - self.serial = serial self.src_port = src_port - self.dst_port = dst_port self.server_socket = None self.inputs = [] - self.enable_ssl = enable_ssl self.stopped = threading.Event() self.listening_event = listening_event - self.usbmux_connection_type = usbmux_connection_type # dictionaries containing the required maps to transfer data between each local # socket to its remote socket and vice versa @@ -111,28 +103,17 @@ def _handle_data(self, from_sock, closed_sockets): other_sock.sendall(data) other_sock.setblocking(False) + @abstractmethod + def _establish_remote_connection(self) -> socket.socket: + pass + def _handle_server_connection(self): """ accept the connection from local machine and attempt to connect at remote """ local_connection, client_address = self.server_socket.accept() local_connection.setblocking(False) try: - if self.enable_ssl: - # use the lockdown pairing record - lockdown = create_using_usbmux(self.serial, connection_type=self.usbmux_connection_type) - service_connection = LockdownServiceConnection.create_using_usbmux( - self.serial, self.dst_port, connection_type=self.usbmux_connection_type) - - with lockdown.ssl_file() as ssl_file: - service_connection.ssl_start(ssl_file) - - remote_connection = service_connection.socket - else: - # connect directly using usbmuxd - mux_device = usbmux.select_device(self.serial, connection_type=self.usbmux_connection_type) - if mux_device is None: - raise ConnectionFailedError() - remote_connection = mux_device.connect(self.dst_port) + remote_connection = self._establish_remote_connection() except ConnectionFailedError: self.logger.error(f'failed to connect to port: {self.dst_port}') local_connection.close() @@ -148,8 +129,59 @@ def _handle_server_connection(self): self.connections[remote_connection] = local_connection self.connections[local_connection] = remote_connection - self.logger.info(f'connection established from local to remote port {self.dst_port}') + self.logger.info('connection established from local to remote') def stop(self): """ stop forwarding """ self.stopped.set() + + +class UsbmuxTcpForwarder(TcpForwarderBase): + """ + Allows forwarding local tcp connection into the device via a given lockdown connection + """ + + def __init__(self, serial: str, dst_port: int, src_port: int, listening_event: threading.Event = None, + usbmux_connection_type: str = None): + """ + Initialize a new tcp forwarder + + :param serial: device serial + :param dst_port: tcp port to connect to each new connection via the supplied lockdown object + :param src_port: tcp port to listen on + :param listening_event: event to fire when the listening occurred + :param usbmux_connection_type: preferred connection type + """ + super().__init__(src_port, listening_event) + self.serial = serial + self.dst_port = dst_port + self.usbmux_connection_type = usbmux_connection_type + + def _establish_remote_connection(self) -> socket.socket: + # connect directly using usbmuxd + mux_device = usbmux.select_device(self.serial, connection_type=self.usbmux_connection_type) + if mux_device is None: + raise ConnectionFailedError() + return mux_device.connect(self.dst_port) + + +class LockdownTcpForwarder(TcpForwarderBase): + """ + Allows forwarding local tcp connection into the device via a given lockdown connection + """ + + def __init__(self, service_provider: LockdownServiceProvider, src_port: int, service_name: str, + listening_event: threading.Event = None): + """ + Initialize a new tcp forwarder + + :param src_port: tcp port to listen on + :param service_name: service name to connect to + :param listening_event: event to fire when the listening occurred + """ + super().__init__(src_port, listening_event) + self.service_provider = service_provider + self.service_name = service_name + + def _establish_remote_connection(self) -> socket.socket: + return self.service_provider.start_lockdown_developer_service(self.service_name).socket diff --git a/tests/services/test_tcp_forwarder.py b/tests/services/test_tcp_forwarder.py index 6afcef6e8..3c78a3299 100644 --- a/tests/services/test_tcp_forwarder.py +++ b/tests/services/test_tcp_forwarder.py @@ -4,7 +4,7 @@ import pytest from pymobiledevice3.lockdown import SERVICE_PORT, LockdownClient -from pymobiledevice3.tcp_forwarder import TcpForwarder +from pymobiledevice3.tcp_forwarder import UsbmuxTcpForwarder FREE_PORT = 3582 @@ -19,7 +19,7 @@ def attempt_local_connection(port: int): def test_tcp_forwarder_bad_port(lockdown: LockdownClient, dst_port: int): # start forwarder listening_event = threading.Event() - forwarder = TcpForwarder(FREE_PORT, dst_port, listening_event=listening_event) + forwarder = UsbmuxTcpForwarder(lockdown.udid, dst_port, FREE_PORT, listening_event=listening_event) thread = threading.Thread(target=forwarder.start) thread.start() From a633b58f5b33b45dcf824d0adfcfcde2998e8b10 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Tue, 22 Aug 2023 23:32:31 +0300 Subject: [PATCH 194/234] HeartbeatService: remove `LockdownService` inheritance The service usage requires to re-establish a connection on every `start()`, making the first service start redundant --- pymobiledevice3/services/heartbeat.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/services/heartbeat.py b/pymobiledevice3/services/heartbeat.py index a34371340..a1011dfc7 100644 --- a/pymobiledevice3/services/heartbeat.py +++ b/pymobiledevice3/services/heartbeat.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 +import logging import time from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider -from pymobiledevice3.services.lockdown_service import LockdownService -class HeartbeatService(LockdownService): +class HeartbeatService: """ Use to keep an active connection with lockdowd """ @@ -14,12 +14,15 @@ class HeartbeatService(LockdownService): RSD_SERVICE_NAME = 'com.apple.mobile.heartbeat.shim.remote' def __init__(self, lockdown: LockdownServiceProvider): + self.logger = logging.getLogger(__name__) + self.lockdown = lockdown + if isinstance(lockdown, LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + self.service_name = self.SERVICE_NAME else: - super().__init__(lockdown, self.RSD_SERVICE_NAME) + self.service_name = self.RSD_SERVICE_NAME - def start(self, interval=None): + def start(self, interval: float = None) -> None: start = time.time() service = self.lockdown.start_lockdown_service(self.service_name) From c26fc2bb3572847bac3aedef3e746d68b19d7e3c Mon Sep 17 00:00:00 2001 From: m1stadev Date: Tue, 22 Aug 2023 15:38:30 -0500 Subject: [PATCH 195/234] requirements: `pyimg4>=0.8` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d8e33d91b..c4deb3094 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ wsproto nest_asyncio>=1.5.5 Pillow inquirer3>=0.1.0 -pyimg4>=0.7 +pyimg4>=0.8 ipsw_parser>=1.1.2 remotezip zeroconf From 05d158d121020da5e5537d8d7ee48506b4bf991c Mon Sep 17 00:00:00 2001 From: m1stadev Date: Tue, 22 Aug 2023 16:11:59 -0500 Subject: [PATCH 196/234] restore: Add in more error handling when (re)connecting to device --- pymobiledevice3/cli/restore.py | 4 ++-- pymobiledevice3/restore/restore.py | 2 +- pymobiledevice3/restore/restored_client.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/cli/restore.py b/pymobiledevice3/cli/restore.py index cb306b220..bfcbd8faa 100644 --- a/pymobiledevice3/cli/restore.py +++ b/pymobiledevice3/cli/restore.py @@ -11,7 +11,7 @@ from pymobiledevice3 import usbmux from pymobiledevice3.cli.cli_common import print_json, set_verbosity -from pymobiledevice3.exceptions import IncorrectModeError +from pymobiledevice3.exceptions import ConnectionFailedError, IncorrectModeError from pymobiledevice3.irecv import IRecv from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux from pymobiledevice3.restore.device import Device @@ -47,7 +47,7 @@ def device(ctx, param, value): for device in usbmux.list_devices(): try: lockdown = create_using_usbmux(serial=device.serial, connection_type='USB') - except IncorrectModeError: + except (ConnectionFailedError, IncorrectModeError): continue if (ecid is None) or (lockdown.ecid == value): logger.debug('found device') diff --git a/pymobiledevice3/restore/restore.py b/pymobiledevice3/restore/restore.py index 88176e58b..1dfe4def2 100644 --- a/pymobiledevice3/restore/restore.py +++ b/pymobiledevice3/restore/restore.py @@ -1181,7 +1181,7 @@ def _connect_to_restored_service(self): try: self._restored = RestoredClient() break - except NoDeviceConnectedError: + except (ConnectionFailedError, NoDeviceConnectedError): pass def restore_device(self): diff --git a/pymobiledevice3/restore/restored_client.py b/pymobiledevice3/restore/restored_client.py index 21262c147..2a39e1905 100644 --- a/pymobiledevice3/restore/restored_client.py +++ b/pymobiledevice3/restore/restored_client.py @@ -14,7 +14,8 @@ class RestoredClient(object): def __init__(self, udid=None, client_name=DEFAULT_CLIENT_NAME): self.logger = logging.getLogger(__name__) self.udid = self._get_or_verify_udid(udid) - self.service = LockdownServiceConnection.create_using_usbmux(self.udid, self.SERVICE_PORT, connection_type='USB') + self.service = LockdownServiceConnection.create_using_usbmux(self.udid, self.SERVICE_PORT, + connection_type='USB') self.label = client_name self.query_type = self.service.send_recv_plist({'Request': 'QueryType'}) self.version = self.query_type.get('RestoreProtocolVersion') From 7b3e26b19421da560a77290ef16b082ffde3ca74 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 23 Aug 2023 08:33:00 +0300 Subject: [PATCH 197/234] pyproject: bump version to 2.6.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7b43b8f7b..8c24bb910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.5.1" +version = "2.6.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 35a082a84d16885b51dc739adf1e680b45d06739 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 24 Aug 2023 07:50:21 +0300 Subject: [PATCH 198/234] cli: fix device selection in `start-quic-tunnel` --- pymobiledevice3/cli/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index aaa5d40ab..fe7fab3e8 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -106,7 +106,7 @@ def cli_start_quic_tunnel(udid: str, secrets: TextIO): rsd = devices[0] else: # several devices were found - if udid is not None: + if udid is None: # show prompt if non explicitly selected rsd = prompt_device_list(devices) else: From ffb3e8514f12b342df1f1b27574b7da923611b5b Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 24 Aug 2023 08:50:51 +0300 Subject: [PATCH 199/234] pyproject: bump version to 2.6.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c24bb910..2d566bd9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.6.0" +version = "2.6.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 8372746fe2fe13e1e813e395bf0b91e6a2f4a07c Mon Sep 17 00:00:00 2001 From: Steve Ames Date: Mon, 28 Aug 2023 10:52:33 -0400 Subject: [PATCH 200/234] RemoteServiceDiscoveryService: make get_service_port() public --- .../remote/remote_service_discovery.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pymobiledevice3/remote/remote_service_discovery.py b/pymobiledevice3/remote/remote_service_discovery.py index 4952bb18d..fb92ae59c 100644 --- a/pymobiledevice3/remote/remote_service_discovery.py +++ b/pymobiledevice3/remote/remote_service_discovery.py @@ -39,7 +39,7 @@ def connect(self) -> None: self.product_type = self.peer_info['Properties']['ProductType'] def start_lockdown_service_without_checkin(self, name: str) -> LockdownServiceConnection: - return LockdownServiceConnection.create_using_tcp(self.service.address[0], self._get_service_port(name)) + return LockdownServiceConnection.create_using_tcp(self.service.address[0], self.get_service_port(name)) def start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: service = self.start_lockdown_service_without_checkin(name) @@ -70,7 +70,7 @@ def start_lockdown_developer_service(self, name, escrow_bag: bytes = None) -> Lo raise def start_remote_service(self, name: str) -> RemoteXPCConnection: - service = RemoteXPCConnection((self.service.address[0], self._get_service_port(name))) + service = RemoteXPCConnection((self.service.address[0], self.get_service_port(name))) return service def start_service(self, name: str) -> Union[RemoteXPCConnection, LockdownServiceConnection]: @@ -79,6 +79,13 @@ def start_service(self, name: str) -> Union[RemoteXPCConnection, LockdownService use_remote_xpc = service_properties.get('UsesRemoteXPC', False) return self.start_remote_service(name) if use_remote_xpc else self.start_lockdown_service(name) + def get_service_port(self, name: str) -> int: + """takes a service name and returns the port that service is running on if the service exists""" + service = self.peer_info['Services'].get(name) + if service is None: + raise InvalidServiceError(f'No such service: {name}') + return int(service['Port']) + def __enter__(self) -> 'RemoteServiceDiscoveryService': self.connect() return self @@ -90,12 +97,6 @@ def __repr__(self) -> str: return (f'<{self.__class__.__name__} PRODUCT:{self.product_type} VERSION:{self.product_version} ' f'UDID:{self.udid}>') - def _get_service_port(self, name: str) -> int: - service = self.peer_info['Services'].get(name) - if service is None: - raise InvalidServiceError(f'No such service: {name}') - return int(service['Port']) - def get_remoted_devices(timeout: int = DEFAULT_BONJOUR_TIMEOUT) -> List[RSDDevice]: result = [] From 401378f1564f4dc951f34c82105989dcb3505e60 Mon Sep 17 00:00:00 2001 From: Steve Ames Date: Mon, 28 Aug 2023 13:38:25 -0400 Subject: [PATCH 201/234] cli: make debugserver start-server port argument optional on iOS 17 --- pymobiledevice3/cli/developer.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/cli/developer.py b/pymobiledevice3/cli/developer.py index 38a822ff5..1afcedf24 100644 --- a/pymobiledevice3/cli/developer.py +++ b/pymobiledevice3/cli/developer.py @@ -9,7 +9,7 @@ from dataclasses import asdict from datetime import datetime from pathlib import Path -from typing import List +from typing import List, Optional import click from click.exceptions import MissingParameter, UsageError @@ -860,10 +860,11 @@ def debugserver_applist(service_provider: LockdownClient): @debugserver.command('start-server', cls=Command) -@click.argument('local_port', type=click.INT) -def debugserver_start_server(service_provider: LockdownClient, local_port): +@click.argument('local_port', type=click.INT, required=False) +def debugserver_start_server(service_provider: LockdownClient, local_port: Optional[int] = None): """ - start a debugserver at remote listening on a given port locally. + if local_port is provided, start a debugserver at remote listening on a given port locally. + if local_port is not provided and iOS version >= 17.0 then just print the connect string Please note the connection must be done soon afterwards using your own lldb client. This can be done using the following commands within lldb shell: @@ -878,7 +879,13 @@ def debugserver_start_server(service_provider: LockdownClient, local_port): else: service_name = 'com.apple.internal.dt.remote.debugproxy' - LockdownTcpForwarder(service_provider, local_port, service_name).start() + if local_port is not None: + LockdownTcpForwarder(service_provider, local_port, service_name).start() + elif Version(service_provider.product_version) >= Version('17.0'): + debugserver_port = service_provider.get_service_port(service_name) + print(f"Connect with: platform connect connect://[{service_provider.service.address[0]}]:{debugserver_port}") + else: + print("local_port is required for iOS < 17.0") @developer.group('arbitration') From 5ac9281ab4319ae79a57d55f8edab3002cd8367e Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 30 Aug 2023 16:58:50 +0300 Subject: [PATCH 202/234] pyproject: bump version to 2.7.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2d566bd9b..a478ff516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.6.1" +version = "2.7.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 4d36ce9d91c04a209c017768f4cbf0d9239f170d Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 3 Sep 2023 08:26:58 +0300 Subject: [PATCH 203/234] requirements: `aioquic-pmd3>=0.0.2` --- README.md | 7 +------ requirements.txt | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5c56a5645..f7a9ff297 100644 --- a/README.md +++ b/README.md @@ -84,22 +84,17 @@ eval "$(_PYMOBILEDEVICE3_COMPLETE=zsh_source pymobiledevice3)" ## OpenSSL libraries -Currently, openssl is explicitly required if using on older iOS version (<13) or creating a QUIC tunnel (iOS>=17). +Currently, openssl is explicitly required if using on older iOS version (<13). On macOS: ```shell -# for iOS>=17 support -brew install openssl@3 - -# for iOS<13 support brew install openssl ``` On Linux: ```shell -# for iOS<13 support sudo apt install openssl ``` diff --git a/requirements.txt b/requirements.txt index c4deb3094..9eda75abb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ zeroconf ifaddr hyperframe srptools -aioquic-pmd3>=0.0.1 ; platform_system != "Windows" +aioquic-pmd3>=0.0.2 ; platform_system != "Windows" developer_disk_image>=0.0.2 opack psutil From d69d88de130431b609c698696e234cb81f4339bd Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 3 Sep 2023 08:49:04 +0300 Subject: [PATCH 204/234] SyslogService: refactor `lockdown` argument to `service_provider` --- pymobiledevice3/cli/syslog.py | 2 +- pymobiledevice3/services/syslog.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/cli/syslog.py b/pymobiledevice3/cli/syslog.py index 7868f6192..50708a32f 100644 --- a/pymobiledevice3/cli/syslog.py +++ b/pymobiledevice3/cli/syslog.py @@ -29,7 +29,7 @@ def syslog(): @syslog.command('live-old', cls=Command) def syslog_live_old(service_provider: LockdownClient): """ view live syslog lines in raw bytes form from old relay """ - for line in SyslogService(lockdown=service_provider).watch(): + for line in SyslogService(service_provider=service_provider).watch(): print(line) diff --git a/pymobiledevice3/services/syslog.py b/pymobiledevice3/services/syslog.py index 4135db917..1358723fd 100644 --- a/pymobiledevice3/services/syslog.py +++ b/pymobiledevice3/services/syslog.py @@ -16,11 +16,11 @@ class SyslogService(LockdownService): SERVICE_NAME = 'com.apple.syslog_relay' RSD_SERVICE_NAME = 'com.apple.syslog_relay.shim.remote' - def __init__(self, lockdown: LockdownServiceProvider): - if isinstance(lockdown, LockdownClient): - super().__init__(lockdown, self.SERVICE_NAME) + def __init__(self, service_provider: LockdownServiceProvider): + if isinstance(service_provider, LockdownClient): + super().__init__(service_provider, self.SERVICE_NAME) else: - super().__init__(lockdown, self.RSD_SERVICE_NAME) + super().__init__(service_provider, self.RSD_SERVICE_NAME) def watch(self): buf = b'' From 09aef494653acf1cba895ba7509e80b2cbd6f3fa Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 3 Sep 2023 08:49:42 +0300 Subject: [PATCH 205/234] README: add an rsd exmaple --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f7a9ff297..54ca93145 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,28 @@ Commands: You could also import the modules and use the API yourself: ```python +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService from pymobiledevice3.lockdown import create_using_usbmux from pymobiledevice3.services.syslog import SyslogService +# Connecting via usbmuxd lockdown = create_using_usbmux() -for line in SyslogService(lockdown=lockdown).watch(): +for line in SyslogService(service_provider=lockdown).watch(): # just print all syslog lines as is print(line) + +# Or via remoted (iOS>=17) +# First, create a tunnel using: +# $ sudo pymobiledevice3 remote start-quic-tunnel +# You can of course implement it yourself by copying the same pieces of code from: +# https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/cli/remote.py#L68 +# Now you can simply connect to the created tunnel's host and port +host = 'fded:c26b:3d2f::1' # randomized +port = 65177 # randomized +with RemoteServiceDiscoveryService((host, port)) as rsd: + for line in SyslogService(service_provider=rsd).watch(): + # just print all syslog lines as is + print(line) ``` ## Working with developer tools (iOS >= 17.0) From 95176e50d06cb4f3931b115a69485c653b38818f Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 3 Sep 2023 09:07:48 +0300 Subject: [PATCH 206/234] pyproject: bump version to 2.8.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a478ff516..2b4fb4c29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.7.0" +version = "2.8.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 218bf6cd1a470bb9acb7bf2a38469b1e1de73aba Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 29 Aug 2023 18:44:47 +0300 Subject: [PATCH 207/234] requirements: replace `aioquic-pmd3` with `qh3` --- .../remote/core_device_tunnel_service.py | 14 +++++++------- requirements.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 2e0df27e1..8e61bc605 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -16,13 +16,6 @@ from typing import AsyncGenerator, List, Mapping, Optional, TextIO, cast import aiofiles -from aioquic_pmd3.asyncio import QuicConnectionProtocol -from aioquic_pmd3.asyncio.client import connect as aioquic_connect -from aioquic_pmd3.asyncio.protocol import QuicStreamHandler -from aioquic_pmd3.quic import packet_builder -from aioquic_pmd3.quic.configuration import QuicConfiguration -from aioquic_pmd3.quic.connection import QuicConnection -from aioquic_pmd3.quic.events import ConnectionTerminated, DatagramFrameReceived, QuicEvent, StreamDataReceived from construct import Const, Container, Enum, GreedyBytes, GreedyRange, Int8ul, Int16ub, Int64ul, Prefixed, Struct from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives._serialization import Encoding, PublicFormat @@ -33,6 +26,13 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from opack import dumps from pytun_pmd3 import TunTapDevice +from qh3.asyncio import QuicConnectionProtocol +from qh3.asyncio.client import connect as aioquic_connect +from qh3.asyncio.protocol import QuicStreamHandler +from qh3.quic import packet_builder +from qh3.quic.configuration import QuicConfiguration +from qh3.quic.connection import QuicConnection +from qh3.quic.events import ConnectionTerminated, DatagramFrameReceived, QuicEvent, StreamDataReceived from srptools import SRPClientSession, SRPContext from srptools.constants import PRIME_3072, PRIME_3072_GEN diff --git a/requirements.txt b/requirements.txt index 9eda75abb..c276683f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ zeroconf ifaddr hyperframe srptools -aioquic-pmd3>=0.0.2 ; platform_system != "Windows" +qh3>=0.11.4 developer_disk_image>=0.0.2 opack psutil From a7b69104ff95225c3200d9b710f0ede0af9ba048 Mon Sep 17 00:00:00 2001 From: doronz Date: Sun, 3 Sep 2023 16:03:16 +0300 Subject: [PATCH 208/234] pyproject: bump version to 2.8.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2b4fb4c29..ec4ff3470 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.8.0" +version = "2.8.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 84e3fcd0c67e08b8d038bd002f5a952129aca185 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 5 Sep 2023 09:07:21 +0300 Subject: [PATCH 209/234] core_device_tunnel_service: remove uses of `bytes.removeprefix()` --- pymobiledevice3/remote/core_device_tunnel_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 8e61bc605..e1ab1a2cd 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -113,7 +113,7 @@ async def tun_read_task(self) -> None: while True: packet = await f.read(read_size) assert packet.startswith(LOOKBACK_HEADER) - packet = packet.removeprefix(LOOKBACK_HEADER) + packet = packet[len(LOOKBACK_HEADER):] self._quic.send_datagram_frame(packet) self.transmit() From d8638e8dcce2fd407a7876cfe0219925982a4cbd Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 5 Sep 2023 09:00:31 +0300 Subject: [PATCH 210/234] core_device_tunnel_service: make asyncio tasks print the exceptions --- .../remote/core_device_tunnel_service.py | 3 +++ pymobiledevice3/utils.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index e1ab1a2cd..3a6c04fd7 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -41,6 +41,7 @@ from pymobiledevice3.remote.remote_service import RemoteService from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type +from pymobiledevice3.utils import asyncio_print_traceback LOOKBACK_HEADER = struct.pack('>I', AF_INET6) @@ -107,6 +108,7 @@ def __init__(self, quic: QuicConnection, stream_handler: Optional[QuicStreamHand self._tun_read_task = None self.tun = None + @asyncio_print_traceback async def tun_read_task(self) -> None: read_size = self.tun.mtu + len(LOOKBACK_HEADER) async with aiofiles.open(self.tun.fileno(), 'rb', opener=lambda path, flags: path, buffering=0) as f: @@ -126,6 +128,7 @@ async def request_tunnel_establish(self) -> Mapping: self.transmit() return await self._queue.get() + @asyncio_print_traceback async def keep_alive_task(self, interval: float) -> None: while True: await self.ping() diff --git a/pymobiledevice3/utils.py b/pymobiledevice3/utils.py index acce0cacd..9710d78ff 100644 --- a/pymobiledevice3/utils.py +++ b/pymobiledevice3/utils.py @@ -1,4 +1,8 @@ +import asyncio import re +import traceback +from functools import wraps +from typing import Callable from construct import Int8ul, Int16ul, Int32ul, Int64ul, Select @@ -41,3 +45,16 @@ def try_decode(s: bytes): return s.decode('utf8') except UnicodeDecodeError: return s + + +def asyncio_print_traceback(f: Callable): + @wraps(f) + async def wrapper(*args, **kwargs): + try: + return await f(*args, **kwargs) + except Exception as e: # noqa: E72 + if not isinstance(e, asyncio.CancelledError): + traceback.print_exc() + raise + + return wrapper From 29d7c2424e54a614033700dfa43005e72cc8fff2 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 5 Sep 2023 09:01:25 +0300 Subject: [PATCH 211/234] core_device_tunnel_service: avoid verifying the device hostname --- pymobiledevice3/remote/core_device_tunnel_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 3a6c04fd7..67ce5b55e 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -221,6 +221,7 @@ async def start_quic_tunnel(self, private_key: RSAPrivateKey, secrets_log_file: certificate=cert, private_key=private_key, verify_mode=VerifyMode.CERT_NONE, + verify_hostname=False, max_datagram_frame_size=RemotePairingTunnel.MAX_QUIC_DATAGRAM, idle_timeout=RemotePairingTunnel.MAX_IDLE_TIMEOUT ) From c2eedf56865d1d771eb853ea709c44fe0ee01cd7 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 5 Sep 2023 10:16:18 +0300 Subject: [PATCH 212/234] requirements: `qh3>=0.11.5` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c276683f9..50824505f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ zeroconf ifaddr hyperframe srptools -qh3>=0.11.4 +qh3>=0.11.5 developer_disk_image>=0.0.2 opack psutil From 5329d470d64d931993deee33e1a12024d3af0041 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 5 Sep 2023 10:27:04 +0300 Subject: [PATCH 213/234] pyproject: bump version to 2.8.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ec4ff3470..731463eb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.8.1" +version = "2.8.2" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From bf2ce4460a2731e5b254915bb94694a180383fcb Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 7 Sep 2023 08:16:26 +0300 Subject: [PATCH 214/234] utils: ignore `NoSuchProcess` from `_get_remoted_process()` (#557) --- pymobiledevice3/remote/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/utils.py b/pymobiledevice3/remote/utils.py index 2b56152cc..5ac872027 100644 --- a/pymobiledevice3/remote/utils.py +++ b/pymobiledevice3/remote/utils.py @@ -17,7 +17,7 @@ def _get_remoted_process() -> psutil.Process: try: if process.exe() == REMOTED_PATH: return process - except psutil.ZombieProcess: + except (psutil.ZombieProcess, psutil.NoSuchProcess): continue From e460a93d6db0d71d5d32af856f4ccf1abf34bd71 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 7 Sep 2023 08:35:00 +0300 Subject: [PATCH 215/234] core_device_tunnel_service: handle `UserDeniedPairingError` (#557) --- pymobiledevice3/remote/core_device_tunnel_service.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 67ce5b55e..1b411d745 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -37,6 +37,7 @@ from srptools.constants import PRIME_3072, PRIME_3072_GEN from pymobiledevice3.ca import make_cert +from pymobiledevice3.exceptions import PyMobileDevice3Exception, UserDeniedPairingError from pymobiledevice3.pair_records import create_pairing_records_cache_folder, generate_host_id from pymobiledevice3.remote.remote_service import RemoteService from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService @@ -288,6 +289,7 @@ def _request_pair_consent(self) -> PairConsentResult: 'kind': 'setupManualPairing', 'sendingHost': platform.node(), 'startNewSession': True}) + self.logger.info('Waiting user pairing consent') assert 'awaitingUserConsent' in self._receive_plain_response()['event']['_0'] response = self._receive_pairing_data() data = self.decode_tlv(PairingDataComponentTLVBuf.parse( @@ -509,7 +511,15 @@ def _send_pairing_data(self, pairing_data: Mapping) -> None: self._send_plain_request({'event': {'_0': {'pairingData': {'_0': pairing_data}}}}) def _receive_pairing_data(self) -> Mapping: - return self._receive_plain_response()['event']['_0']['pairingData']['_0']['data'] + response = self._receive_plain_response()['event']['_0'] + if 'pairingData' in response: + return response['pairingData']['_0']['data'] + if 'pairingRejectedWithError' in response: + raise UserDeniedPairingError(response['pairingRejectedWithError'] + .get('wrappedError', {}) + .get('userInfo', {}) + .get('NSLocalizedDescription')) + raise PyMobileDevice3Exception(f'Got an unknown state message: {response}') def _send_receive_plain_request(self, plain_request: Mapping): self._send_plain_request(plain_request) From bcc5650d5f72b93a5b7b871b59afc4f6800dddca Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 7 Sep 2023 08:46:57 +0300 Subject: [PATCH 216/234] remotexpc: handle `ConnectionAbortedError` from `_recvall()` --- pymobiledevice3/remote/remotexpc.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pymobiledevice3/remote/remotexpc.py b/pymobiledevice3/remote/remotexpc.py index a61fee6b9..f0cdc1bd8 100644 --- a/pymobiledevice3/remote/remotexpc.py +++ b/pymobiledevice3/remote/remotexpc.py @@ -169,7 +169,10 @@ def _receive_frame(self) -> Frame: return frame def _recvall(self, size: int) -> bytes: - buf = b'' - while len(buf) < size: - buf += self.sock.recv(size - len(buf)) - return buf + data = b'' + while len(data) < size: + chunk = self.sock.recv(size - len(data)) + if chunk is None or len(chunk) == 0: + raise ConnectionAbortedError() + data += chunk + return data From 07ad5dee35eb60f66a65a75c4c95506120de9438 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 7 Sep 2023 14:06:37 +0300 Subject: [PATCH 217/234] requirements: `pytun-pmd3>=1.0.0` --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 50824505f..d47e7def9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,5 +37,5 @@ qh3>=0.11.5 developer_disk_image>=0.0.2 opack psutil -pytun-pmd3>=0.0.2 ; platform_system != "Windows" +pytun-pmd3>=1.0.0 ; platform_system != "Windows" aiofiles From 644f6d30f85ccf959be4119fc7a37fe122ab27fc Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 7 Sep 2023 14:06:48 +0300 Subject: [PATCH 218/234] core_device_tunnel_service: fix linux support --- pymobiledevice3/remote/core_device_tunnel_service.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index 1b411d745..c549ea382 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -7,6 +7,7 @@ import platform import plistlib import struct +import sys from asyncio import CancelledError from collections import namedtuple from contextlib import asynccontextmanager, suppress @@ -44,7 +45,10 @@ from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type from pymobiledevice3.utils import asyncio_print_traceback -LOOKBACK_HEADER = struct.pack('>I', AF_INET6) +if sys.platform == 'darwin': + LOOKBACK_HEADER = struct.pack('>I', AF_INET6) +else: + LOOKBACK_HEADER = b'\x00\x00\x86\xdd' # The iOS device uses an MTU of 1500, so we'll have to increase the default QUIC MTU packet_builder.PACKET_MAX_SIZE = 1452 # 1500 - 40byte ipv6 - 8 byte udp @@ -138,7 +142,8 @@ async def keep_alive_task(self, interval: float) -> None: def start_tunnel(self, address: str, mtu: int) -> None: self.tun = TunTapDevice() self.tun.mtu = mtu - self.tun.addr6 = address + self.tun.addr = address + self.tun.up() self._keep_alive_task = asyncio.create_task(self.keep_alive_task(self.MAX_IDLE_TIMEOUT / 2)) self._tun_read_task = asyncio.create_task(self.tun_read_task()) @@ -149,6 +154,8 @@ async def stop_tunnel(self) -> None: await self._keep_alive_task with suppress(CancelledError): await self._tun_read_task + if sys.platform != 'darwin': + self.tun.down() self.tun = None def quic_event_received(self, event: QuicEvent) -> None: From 07520be21a1d3c247124cc549f0424657f36e554 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 7 Sep 2023 16:57:54 +0300 Subject: [PATCH 219/234] pyproject: bump version to 2.8.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 731463eb5..d88036bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.8.2" +version = "2.8.3" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From b9055ccdf66a330cfe820e55f67a6feb8d415304 Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 11 Sep 2023 16:47:13 +0300 Subject: [PATCH 220/234] cli: fix src and dst port forward swap --- pymobiledevice3/cli/usbmux.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/usbmux.py b/pymobiledevice3/cli/usbmux.py index c27b708aa..31b784430 100644 --- a/pymobiledevice3/cli/usbmux.py +++ b/pymobiledevice3/cli/usbmux.py @@ -30,7 +30,7 @@ def usbmux_cli(): @click.option('-d', '--daemonize', is_flag=True) def usbmux_forward(src_port: int, dst_port: int, serial: str, daemonize: bool): """ forward tcp port """ - forwarder = UsbmuxTcpForwarder(serial, src_port, dst_port) + forwarder = UsbmuxTcpForwarder(serial, dst_port, src_port) if daemonize: try: From 79750b78ba4a36fe2e208066588c94d7a7c424bc Mon Sep 17 00:00:00 2001 From: doronz Date: Mon, 11 Sep 2023 17:39:49 +0300 Subject: [PATCH 221/234] pyproject: bump version to 2.8.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d88036bb6..64cfed363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.8.3" +version = "2.8.4" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 61cdcf5b67f409d06c6106b7b8fa7b81ec19e13f Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 21 Sep 2023 16:54:19 +0300 Subject: [PATCH 222/234] cli: ignore devices with RSD connection errors in `remote browse` --- pymobiledevice3/cli/remote.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index fe7fab3e8..99ed5aa39 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -27,7 +27,10 @@ def get_device_list() -> List[RemoteServiceDiscoveryService]: with stop_remoted(): for address in get_remoted_addresses(): rsd = RemoteServiceDiscoveryService((address, RSD_PORT)) - rsd.connect() + try: + rsd.connect() + except ConnectionRefusedError: + continue result.append(rsd) return result From e515bdf4cf89f2569c98d3393e79873b88e177da Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 21 Sep 2023 16:57:24 +0300 Subject: [PATCH 223/234] cli: fix `NotImplementedError` while browsing for remote devices --- pymobiledevice3/remote/bonjour.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pymobiledevice3/remote/bonjour.py b/pymobiledevice3/remote/bonjour.py index 9dfd4a324..f0c9e1012 100644 --- a/pymobiledevice3/remote/bonjour.py +++ b/pymobiledevice3/remote/bonjour.py @@ -24,6 +24,12 @@ def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: if entry.type == _TYPE_AAAA: self.addresses.append(inet_ntop(AF_INET6, entry.address) + '%' + self.adapter.nice_name) + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + pass + + def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: + pass + @dataclasses.dataclass class BonjourQuery: From 2604fda3064ede09986a32d7d6fd91789dba302f Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 21 Sep 2023 19:56:20 +0300 Subject: [PATCH 224/234] pyproject: bump version to 2.8.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 64cfed363..3ec11eeec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.8.4" +version = "2.8.5" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 835ace0c840a062f49fc5f7ca38678d4136eb516 Mon Sep 17 00:00:00 2001 From: m1stadev Date: Fri, 22 Sep 2023 10:34:05 -0500 Subject: [PATCH 225/234] irecv_devices: Add iPhone 15/Plus/Pro/Max --- pymobiledevice3/irecv_devices.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymobiledevice3/irecv_devices.py b/pymobiledevice3/irecv_devices.py index bfcd7a6d0..3987accf9 100644 --- a/pymobiledevice3/irecv_devices.py +++ b/pymobiledevice3/irecv_devices.py @@ -108,6 +108,14 @@ display_name='iPhone 14 Pro'), IRecvDevice(product_type='iPhone15,3', hardware_model='d74ap', board_id=0x0E, chip_id=0x8120, display_name='iPhone 14 Pro Max'), + IRecvDevice(product_type='iPhone15,4', hardware_model='d37ap', board_id=0x08, chip_id=0x8120, + display_name='iPhone 15'), + IRecvDevice(product_type='iPhone15,5', hardware_model='d38ap', board_id=0x0A, chip_id=0x8120, + display_name='iPhone 15 Plus'), + IRecvDevice(product_type='iPhone16,1', hardware_model='d83ap', board_id=0x04, chip_id=0x8130, + display_name='iPhone 15 Pro'), + IRecvDevice(product_type='iPhone16,2', hardware_model='d84ap', board_id=0x06, chip_id=0x8130, + display_name='iPhone 15 Pro Max'), # iPod IRecvDevice(product_type='iPod1,1', hardware_model='n45ap', board_id=0x02, chip_id=0x8900, display_name='iPod Touch (1st gen)'), From 4b93980bc206e2d901d75453705a50815709d2c0 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 26 Sep 2023 09:09:04 +0300 Subject: [PATCH 226/234] utils: remove `sanitized_ios_version()` --- pymobiledevice3/exceptions.py | 6 +----- pymobiledevice3/lockdown.py | 5 ----- pymobiledevice3/services/mobile_image_mounter.py | 2 +- pymobiledevice3/utils.py | 10 ---------- tests/services/test_dvt_secure_socket_proxy.py | 2 +- 5 files changed, 3 insertions(+), 22 deletions(-) diff --git a/pymobiledevice3/exceptions.py b/pymobiledevice3/exceptions.py index ff5722be6..4637cf0f1 100644 --- a/pymobiledevice3/exceptions.py +++ b/pymobiledevice3/exceptions.py @@ -1,5 +1,5 @@ __all__ = [ - 'PyMobileDevice3Exception', 'DeviceVersionNotSupportedError', 'IncorrectModeError', 'DeviceVersionFormatError', + 'PyMobileDevice3Exception', 'DeviceVersionNotSupportedError', 'IncorrectModeError', 'NotTrustedError', 'PairingError', 'NotPairedError', 'CannotStopSessionError', 'PasswordRequiredError', 'StartServiceError', 'FatalPairingError', 'NoDeviceConnectedError', 'MuxException', 'MuxVersionError', 'ArgumentError', 'AfcException', 'AfcFileNotFoundError', 'DvtException', 'DvtDirListError', @@ -27,10 +27,6 @@ class IncorrectModeError(PyMobileDevice3Exception): pass -class DeviceVersionFormatError(PyMobileDevice3Exception): - pass - - class NotTrustedError(PyMobileDevice3Exception): pass diff --git a/pymobiledevice3/lockdown.py b/pymobiledevice3/lockdown.py index 47eac952a..3c747eefa 100755 --- a/pymobiledevice3/lockdown.py +++ b/pymobiledevice3/lockdown.py @@ -26,7 +26,6 @@ get_preferred_pair_record from pymobiledevice3.service_connection import LockdownServiceConnection from pymobiledevice3.usbmux import PlistMuxConnection -from pymobiledevice3.utils import sanitize_ios_version SYSTEM_BUID = '30142955-444094379208051516' DOMAINS = ['com.apple.disk_usage', @@ -263,10 +262,6 @@ def locale(self) -> str: def preflight_info(self) -> Mapping: return self.get_value(key='FirmwarePreflightInfo') - @property - def sanitized_ios_version(self) -> str: - return sanitize_ios_version(self.product_version) - @property def display_name(self) -> str: for irecv_device in IRECV_DEVICES: diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index 5bd4ea390..02ccf9f3c 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -302,7 +302,7 @@ def auto_mount_developer(lockdown: LockdownClient, xcode: str = None, version: s raise AlreadyMountedError() if version is None: - version = lockdown.sanitized_ios_version + version = lockdown.product_version image_dir = f'{xcode}/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/{version}' image_path = f'{image_dir}/DeveloperDiskImage.dmg' signature = f'{image_path}.signature' diff --git a/pymobiledevice3/utils.py b/pymobiledevice3/utils.py index 9710d78ff..43726c781 100644 --- a/pymobiledevice3/utils.py +++ b/pymobiledevice3/utils.py @@ -1,13 +1,10 @@ import asyncio -import re import traceback from functools import wraps from typing import Callable from construct import Int8ul, Int16ul, Int32ul, Int64ul, Select -from pymobiledevice3.exceptions import DeviceVersionFormatError - def plist_access_path(d, path: tuple, type_=None, required=False): for component in path: @@ -33,13 +30,6 @@ def bytes_to_uint(b: bytes): return Select(u64=Int64ul, u32=Int32ul, u16=Int16ul, u8=Int8ul).parse(b) -def sanitize_ios_version(version: str): - try: - return re.match(r'\d*\.\d*', version)[0] - except TypeError as e: - raise DeviceVersionFormatError from e - - def try_decode(s: bytes): try: return s.decode('utf8') diff --git a/tests/services/test_dvt_secure_socket_proxy.py b/tests/services/test_dvt_secure_socket_proxy.py index 6cdd476b4..2eb3b2f77 100644 --- a/tests/services/test_dvt_secure_socket_proxy.py +++ b/tests/services/test_dvt_secure_socket_proxy.py @@ -20,7 +20,7 @@ def mount_developer_disk_image(): with DeveloperDiskImageMounter(lockdown=lockdown) as mounter: if mounter.is_image_mounted('Developer'): yield - image_path = DEVICE_SUPPORT / mounter.lockdown.sanitized_ios_version / 'DeveloperDiskImage.dmg' + image_path = DEVICE_SUPPORT / mounter.lockdown.product_version / 'DeveloperDiskImage.dmg' try: mounter.mount(image_path, image_path.with_suffix('.dmg.signature')) except AlreadyMountedError: From f91609440e5ef30475eba3362966c2045e388169 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 26 Sep 2023 09:10:08 +0300 Subject: [PATCH 227/234] LockdownServiceProvider: add `ecid` property --- pymobiledevice3/lockdown_service_provider.py | 5 +++++ pymobiledevice3/remote/remote_service_discovery.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/pymobiledevice3/lockdown_service_provider.py b/pymobiledevice3/lockdown_service_provider.py index 1e197d304..a9919b49d 100644 --- a/pymobiledevice3/lockdown_service_provider.py +++ b/pymobiledevice3/lockdown_service_provider.py @@ -16,6 +16,11 @@ def __init__(self): def product_version(self) -> str: pass + @property + @abstractmethod + def ecid(self) -> int: + pass + @abstractmethod def start_lockdown_service(self, name: str, escrow_bag: bytes = None) -> LockdownServiceConnection: pass diff --git a/pymobiledevice3/remote/remote_service_discovery.py b/pymobiledevice3/remote/remote_service_discovery.py index fb92ae59c..a9d0c6645 100644 --- a/pymobiledevice3/remote/remote_service_discovery.py +++ b/pymobiledevice3/remote/remote_service_discovery.py @@ -32,6 +32,10 @@ def __init__(self, address: Tuple[str, int]): def product_version(self) -> str: return self.peer_info['Properties']['OSVersion'] + @property + def ecid(self) -> int: + return self.peer_info['Properties']['UniqueChipID'] + def connect(self) -> None: self.service.connect() self.peer_info = self.service.receive_response() From dd7e85478c4b839b3562965713d1b8022f1e7356 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 26 Sep 2023 09:10:24 +0300 Subject: [PATCH 228/234] mounter: fix mounting over rsd --- pymobiledevice3/cli/mounter.py | 3 ++- pymobiledevice3/services/mobile_image_mounter.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/cli/mounter.py b/pymobiledevice3/cli/mounter.py index e999fc3fd..3b1c0f064 100644 --- a/pymobiledevice3/cli/mounter.py +++ b/pymobiledevice3/cli/mounter.py @@ -9,6 +9,7 @@ from pymobiledevice3.exceptions import AlreadyMountedError, DeveloperDiskImageNotFoundError, NotMountedError, \ UnsupportedCommandError from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.services.mobile_image_mounter import DeveloperDiskImageMounter, MobileImageMounterService, \ PersonalizedImageMounter, auto_mount @@ -115,7 +116,7 @@ def mounter_mount_personalized(service_provider: LockdownClient, image: str, tru help='Xcode application path used to figure out automatically the DeveloperDiskImage path') @click.option('-v', '--version', help='use a different DeveloperDiskImage version from the one retrieved by lockdown' 'connection') -def mounter_auto_mount(service_provider: LockdownClient, xcode: str, version: str): +def mounter_auto_mount(service_provider: LockdownServiceProvider, xcode: str, version: str): """ auto-detect correct DeveloperDiskImage and mount it """ try: auto_mount(service_provider, xcode=xcode, version=version) diff --git a/pymobiledevice3/services/mobile_image_mounter.py b/pymobiledevice3/services/mobile_image_mounter.py index 02ccf9f3c..f87181422 100755 --- a/pymobiledevice3/services/mobile_image_mounter.py +++ b/pymobiledevice3/services/mobile_image_mounter.py @@ -202,7 +202,7 @@ def mount(self, image: Path, build_manifest: Path, trust_cache: Path, try: manifest = self.query_personalization_manifest('DeveloperDiskImage', hashlib.sha384(image).digest()) except MissingManifestError: - self.service = self.lockdown.start_lockdown_service(self.SERVICE_NAME) + self.service = self.lockdown.start_lockdown_service(self.service_name) manifest = self.get_manifest_from_tss(plistlib.loads(build_manifest.read_bytes())) self.upload_image(self.IMAGE_TYPE, image, manifest) @@ -288,7 +288,7 @@ def get_manifest_from_tss(self, build_manifest: Mapping) -> bytes: return response['ApImg4Ticket'] -def auto_mount_developer(lockdown: LockdownClient, xcode: str = None, version: str = None) -> None: +def auto_mount_developer(lockdown: LockdownServiceProvider, xcode: str = None, version: str = None) -> None: """ auto-detect correct DeveloperDiskImage and mount it """ if xcode is None: # avoid "default"-ing this option, because Windows and Linux won't have this path @@ -327,7 +327,7 @@ def auto_mount_developer(lockdown: LockdownClient, xcode: str = None, version: s image_mounter.mount(image_path, signature) -def auto_mount_personalized(lockdown: LockdownClient) -> None: +def auto_mount_personalized(lockdown: LockdownServiceProvider) -> None: local_path = get_home_folder() / 'Xcode_iOS_DDI_Personalized' local_path.mkdir(parents=True, exist_ok=True) @@ -347,7 +347,7 @@ def auto_mount_personalized(lockdown: LockdownClient) -> None: PersonalizedImageMounter(lockdown=lockdown).mount(image, build_manifest, trustcache) -def auto_mount(lockdown: LockdownClient, xcode: str = None, version: str = None) -> None: +def auto_mount(lockdown: LockdownServiceProvider, xcode: str = None, version: str = None) -> None: if Version(lockdown.product_version) < Version('17.0'): auto_mount_developer(lockdown, xcode=xcode, version=version) else: From 55d9a5a0b2d99bde486ce3422ecedbd553607255 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 26 Sep 2023 10:13:26 +0300 Subject: [PATCH 229/234] pyproject: bump version to 2.9.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ec11eeec..0454cf67f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.8.5" +version = "2.9.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From b56337f4a8513fe1ba69d0f542b0f18c52537720 Mon Sep 17 00:00:00 2001 From: "a.zhuravlov.galchenko" Date: Tue, 26 Sep 2023 14:35:18 +0300 Subject: [PATCH 230/234] Added sample code, how to build exe with this library --- README.md | 5 +++++ misc/py2exe.md | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 misc/py2exe.md diff --git a/README.md b/README.md index 54ca93145..a67bb2719 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [Lockdown messages](#lockdown-messages) - [Instruments messages](#instruments-messages) - [Contributing](#contributing) +- [Useful info](#Useful-info) # News @@ -446,3 +447,7 @@ return_value, auxiliary = developer.recv_plist() # Contributing See [CONTRIBUTING](CONTRIBUTING.md). + +# Useful info + +Please see [misc](misc) \ No newline at end of file diff --git a/misc/py2exe.md b/misc/py2exe.md new file mode 100644 index 000000000..1ce9c4bab --- /dev/null +++ b/misc/py2exe.md @@ -0,0 +1,37 @@ +# Building Windows executable (exe) + +Below is the snippet, which shows how to build exe from python code +This method tested on Windows 10, 11 +Requires module py2exe 0.13.0.0+ + +`options['includes']` Here you specify which modules to include. It will vary depending on your script. General rule - try to build, read errors which modules are missing, include them, build once again. After building - try to run and check for errors for missing modules, if any include them and rebuild. + +```python +from py2exe import freeze +import shutil +import os + +out_dir = "path/for/output" +if os.path.exists(out_dir): + shutil.rmtree(out_dir) + +freeze( + console=['path/to/your/python/scritps.py'], + windows=[], + data_files=None, + zipfile='library.zip', + options={ + 'includes': [ + 'sys', + 'pymobiledevice3', + 'pygments.lexers.python', + 'pygments.lexers.data', + 'charset_normalizer', + 'jinxed.terminfo.vtwin10', + 'pyreadline3', + ], + 'dist_dir': out_dir + }, + version_info={} +) +``` \ No newline at end of file From f377968bd4e65f140a746bdf84eeb5bfd1d59010 Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 26 Sep 2023 20:15:55 +0300 Subject: [PATCH 231/234] requirements: add `gnureadline`/`pyreadline3` --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index d47e7def9..26bb8e890 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,3 +39,5 @@ opack psutil pytun-pmd3>=1.0.0 ; platform_system != "Windows" aiofiles +gnureadline ; platform_system != "Windows" +pyreadline3 ; platform_system == "Windows" From 42c9d8b96a100afa322ac5a73a71a41bb3b45b02 Mon Sep 17 00:00:00 2001 From: DoronZ Date: Wed, 27 Sep 2023 22:28:00 +0300 Subject: [PATCH 232/234] cli: fix `apps afc` subcommand (#582) --- pymobiledevice3/cli/apps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/cli/apps.py b/pymobiledevice3/cli/apps.py index 1b33c88d7..d2df11a5b 100644 --- a/pymobiledevice3/cli/apps.py +++ b/pymobiledevice3/cli/apps.py @@ -51,6 +51,6 @@ def install(service_provider: LockdownClient, ipa_path): @apps.command('afc', cls=Command) @click.argument('bundle_id') -def afc(lockdown: LockdownClient, bundle_id): +def afc(service_provider: LockdownClient, bundle_id): """ open an AFC shell for given bundle_id, assuming its profile is installed """ - HouseArrestService(lockdown=lockdown).shell(bundle_id) + HouseArrestService(lockdown=service_provider).shell(bundle_id) From a7caf76593a2792adcd56f578748b12b5a08e0dc Mon Sep 17 00:00:00 2001 From: DoronZ Date: Wed, 27 Sep 2023 23:30:21 +0300 Subject: [PATCH 233/234] house_arrest: add `documents_only` (#582) --- pymobiledevice3/cli/apps.py | 5 ++-- pymobiledevice3/services/house_arrest.py | 33 +++++++++++++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/pymobiledevice3/cli/apps.py b/pymobiledevice3/cli/apps.py index d2df11a5b..b05807505 100644 --- a/pymobiledevice3/cli/apps.py +++ b/pymobiledevice3/cli/apps.py @@ -50,7 +50,8 @@ def install(service_provider: LockdownClient, ipa_path): @apps.command('afc', cls=Command) +@click.option('--documents', is_flag=True) @click.argument('bundle_id') -def afc(service_provider: LockdownClient, bundle_id): +def afc(service_provider: LockdownClient, bundle_id: str, documents: bool): """ open an AFC shell for given bundle_id, assuming its profile is installed """ - HouseArrestService(lockdown=service_provider).shell(bundle_id) + HouseArrestService(lockdown=service_provider).shell(bundle_id, documents_only=documents) diff --git a/pymobiledevice3/services/house_arrest.py b/pymobiledevice3/services/house_arrest.py index 60b078f8f..d2129df06 100755 --- a/pymobiledevice3/services/house_arrest.py +++ b/pymobiledevice3/services/house_arrest.py @@ -1,7 +1,13 @@ +from pymobiledevice3.exceptions import PyMobileDevice3Exception from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.services.afc import AfcService, AfcShell +VEND_CONTAINER = 'VendContainer' +VEND_DOCUMENTS = 'VendDocuments' + +DOCUMENTS_ROOT = '/Documents' + class HouseArrestService(AfcService): SERVICE_NAME = 'com.apple.mobile.house_arrest' @@ -13,16 +19,19 @@ def __init__(self, lockdown: LockdownServiceProvider): else: super().__init__(lockdown, self.RSD_SERVICE_NAME) - def send_command(self, bundle_id, cmd='VendContainer'): - self.service.send_plist({'Command': cmd, 'Identifier': bundle_id}) - res = self.service.recv_plist() - if res.get('Error'): - self.logger.error('%s: %s', bundle_id, res.get('Error')) - return False - else: - return True + def send_command(self, bundle_id: str, cmd: str = 'VendContainer') -> None: + response = self.service.send_recv_plist({'Command': cmd, 'Identifier': bundle_id}) + error = response.get('Error') + if error: + raise PyMobileDevice3Exception(error) - def shell(self, application_id, cmd='VendContainer'): - res = self.send_command(application_id, cmd) - if res: - AfcShell(self.lockdown, afc_service=self).cmdloop() + def shell(self, bundle_id: str, documents_only: bool = False) -> None: + if documents_only: + cmd = VEND_DOCUMENTS + else: + cmd = VEND_CONTAINER + self.send_command(bundle_id, cmd) + afc_shell = AfcShell(self.lockdown, afc_service=self) + if documents_only: + afc_shell.do_cd(DOCUMENTS_ROOT) + afc_shell.cmdloop() From e8fa08048585c13685446e8d9e891658887966e3 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 28 Sep 2023 11:42:16 +0300 Subject: [PATCH 234/234] pyproject: bump version to 2.10.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0454cf67f..1303e9856 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.9.0" +version = "2.10.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8"