From 754e5e961576c11e6d07e3e15397ecc46a844e2a Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 17 Oct 2023 11:18:33 +0300 Subject: [PATCH 01/13] tests: fix tests to work with ios17 (requiring `--rsd` option) --- .../services/dvt/dvt_secure_socket_proxy.py | 6 +- tests/conftest.py | 47 ++++++++ tests/services/conftest.py | 12 --- .../instruments/test_core_profile_session.py | 9 +- tests/services/test_backup2.py | 5 + tests/services/test_bonjour.py | 4 + .../services/test_dvt_secure_socket_proxy.py | 101 ++++++------------ tests/services/test_screenshotr_relay.py | 9 +- tests/test_utils.py | 14 --- 9 files changed, 96 insertions(+), 111 deletions(-) create mode 100644 tests/conftest.py delete mode 100644 tests/services/conftest.py delete mode 100644 tests/test_utils.py diff --git a/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py b/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py index a6a56efd5..0ee836f2a 100644 --- a/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +++ b/pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py @@ -1,8 +1,6 @@ -from typing import Union - from packaging.version import Version -from pymobiledevice3.lockdown import LockdownClient +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService from pymobiledevice3.services.remote_server import RemoteServer @@ -12,7 +10,7 @@ class DvtSecureSocketProxyService(RemoteServer): OLD_SERVICE_NAME = 'com.apple.instruments.remoteserver' RSD_SERVICE_NAME = 'com.apple.instruments.dtservicehub' - def __init__(self, lockdown: Union[RemoteServiceDiscoveryService, LockdownClient]): + def __init__(self, lockdown: LockdownServiceProvider): if isinstance(lockdown, RemoteServiceDiscoveryService): service_name = self.RSD_SERVICE_NAME remove_ssl_context = False diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..9e3928c83 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,47 @@ +import pytest + +from pymobiledevice3.exceptions import InvalidServiceError +from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux +from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider +from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService +from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService + + +def pytest_addoption(parser): + parser.addoption('--rsd', default=None, type=str, nargs=2, action='store') + + +@pytest.fixture(scope='function') +def service_provider(request) -> LockdownServiceProvider: + """ + Creates a new LockdownServiceProvider client for each test. + """ + rsd = request.config.getoption('--rsd') + + if rsd is not None: + with RemoteServiceDiscoveryService(rsd) as rsd: + yield rsd + else: + with create_using_usbmux() as client: + yield client + + +@pytest.fixture(scope='function') +def dvt(service_provider) -> DvtSecureSocketProxyService: + """ + Creates a new DvtSecureSocketProxyService client for each test. + """ + try: + with DvtSecureSocketProxyService(lockdown=service_provider) as dvt: + yield dvt + except InvalidServiceError: + pytest.skip('Skipping DVT-based test since the service isn\'t accessible') + + +@pytest.fixture(scope='function') +def lockdown(request) -> LockdownClient: + """ + Creates a new lockdown client for each test. + """ + with create_using_usbmux() as client: + yield client diff --git a/tests/services/conftest.py b/tests/services/conftest.py deleted file mode 100644 index e95c91945..000000000 --- a/tests/services/conftest.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest - -from pymobiledevice3.lockdown import create_using_usbmux - - -@pytest.fixture(scope='function') -def lockdown(): - """ - Creates a new lockdown client for each test. - """ - with create_using_usbmux() as client: - yield client diff --git a/tests/services/instruments/test_core_profile_session.py b/tests/services/instruments/test_core_profile_session.py index 54cb23c05..64c75b23a 100644 --- a/tests/services/instruments/test_core_profile_session.py +++ b/tests/services/instruments/test_core_profile_session.py @@ -1,15 +1,12 @@ -from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService from pymobiledevice3.services.dvt.instruments.core_profile_session_tap import CoreProfileSessionTap -def test_stackshot(lockdown): +def test_stackshot(dvt): """ Test getting stackshot. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - with CoreProfileSessionTap(dvt, CoreProfileSessionTap.get_time_config(dvt)) as tap: - data = tap.get_stackshot() + with CoreProfileSessionTap(dvt, CoreProfileSessionTap.get_time_config(dvt)) as tap: + data = tap.get_stackshot() assert 'Darwin Kernel' in data['osversion'] # Constant kernel task data. diff --git a/tests/services/test_backup2.py b/tests/services/test_backup2.py index 13e3ec41f..0e25c244a 100644 --- a/tests/services/test_backup2.py +++ b/tests/services/test_backup2.py @@ -3,6 +3,8 @@ from ssl import SSLEOFError from typing import Callable +import pytest + from pymobiledevice3.exceptions import ConnectionFailedError from pymobiledevice3.lockdown import LockdownClient from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service @@ -15,6 +17,7 @@ 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: @@ -38,10 +41,12 @@ def backup(lockdown: LockdownClient, backup_directory: Path) -> None: service.backup(full=True, backup_directory=backup_directory) +@pytest.mark.filterwarnings('ignore::UserWarning') def test_backup(lockdown, tmp_path): backup(lockdown, tmp_path) +@pytest.mark.filterwarnings('ignore::UserWarning') def test_encrypted_backup(lockdown, tmp_path): change_password(lockdown, new=PASSWORD) backup(lockdown, tmp_path) diff --git a/tests/services/test_bonjour.py b/tests/services/test_bonjour.py index 892c72d0c..46f068640 100644 --- a/tests/services/test_bonjour.py +++ b/tests/services/test_bonjour.py @@ -1,3 +1,5 @@ +import time + from pymobiledevice3.bonjour import browse BROWSE_TIMEOUT = 1 @@ -5,4 +7,6 @@ def test_bonjour(lockdown): lockdown.enable_wifi_connections = True + # give the os some time to start the bonjour broadcast + time.sleep(3) assert len(browse(BROWSE_TIMEOUT).keys()) >= 1 diff --git a/tests/services/test_dvt_secure_socket_proxy.py b/tests/services/test_dvt_secure_socket_proxy.py index 2eb3b2f77..a437a0f92 100644 --- a/tests/services/test_dvt_secure_socket_proxy.py +++ b/tests/services/test_dvt_secure_socket_proxy.py @@ -1,139 +1,100 @@ -from pathlib import Path +import time import pytest -from pymobiledevice3.exceptions import AlreadyMountedError, DvtDirListError, UnrecognizedSelectorError -from pymobiledevice3.lockdown import create_using_usbmux -from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService +from pymobiledevice3.exceptions import DvtDirListError, UnrecognizedSelectorError 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 DeveloperDiskImageMounter -DEVICE_SUPPORT = Path('/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport') -IMAGE_TYPE = 'Developer' - -@pytest.fixture(scope='module', autouse=True) -def mount_developer_disk_image(): - with create_using_usbmux() as lockdown: - with DeveloperDiskImageMounter(lockdown=lockdown) as mounter: - if mounter.is_image_mounted('Developer'): - yield - image_path = DEVICE_SUPPORT / mounter.lockdown.product_version / 'DeveloperDiskImage.dmg' - try: - mounter.mount(image_path, image_path.with_suffix('.dmg.signature')) - except AlreadyMountedError: - pass - - -def get_process_data(lockdown, name): - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - processes = DeviceInfo(dvt).proclist() +def get_process_data(dvt, name: str): + processes = DeviceInfo(dvt).proclist() return [process for process in processes if process['name'] == name][0] -def test_ls(lockdown): +def test_ls(dvt): """ Test listing a directory. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - ls = set(DeviceInfo(dvt).ls('/')) + ls = set(DeviceInfo(dvt).ls('/')) assert {'usr', 'bin', 'etc', 'var', 'private', 'Applications', 'Developer'} <= ls -def test_ls_failure(lockdown): +def test_ls_failure(dvt): """ Test listing a directory. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - with pytest.raises(DvtDirListError): - DeviceInfo(dvt).ls('Directory that does not exist') + with pytest.raises(DvtDirListError): + DeviceInfo(dvt).ls('Directory that does not exist') -def test_proclist(lockdown): +def test_proclist(dvt): """ Test listing processes. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - lockdownd = get_process_data(lockdown, 'lockdownd') + lockdownd = get_process_data(dvt, 'lockdownd') assert lockdownd['realAppName'] == '/usr/libexec/lockdownd' assert not lockdownd['isApplication'] -def test_applist(lockdown): +def test_applist(dvt): """ Test listing applications. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - apps = ApplicationListing(dvt).applist() - + apps = ApplicationListing(dvt).applist() safari = [app for app in apps if app['DisplayName'] == 'StocksWidget'][0] assert safari['CFBundleIdentifier'] == 'com.apple.stocks.widget' assert safari['Restricted'] == 1 assert safari['Type'] == 'PluginKit' -def test_kill(lockdown): +def test_kill(dvt): """ Test killing a process. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - aggregated = get_process_data(lockdown, 'aggregated') - - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - ProcessControl(dvt).kill(aggregated['pid']) - - aggregated_after_kill = get_process_data(lockdown, 'aggregated') + aggregated = get_process_data(dvt, 'SpringBoard') + ProcessControl(dvt).kill(aggregated['pid']) + # give the os some time to start the process again + time.sleep(3) + aggregated_after_kill = get_process_data(dvt, 'SpringBoard') if 'startDate' in aggregated: assert aggregated['startDate'] < aggregated_after_kill['startDate'] -def test_launch(lockdown): +def test_launch(dvt): """ Test launching a process. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - pid = ProcessControl(dvt).launch('com.apple.mobilesafari') - assert pid - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - for process in DeviceInfo(dvt).proclist(): - if pid == process['pid']: - assert process['name'] == 'MobileSafari' + pid = ProcessControl(dvt).launch('com.apple.mobilesafari') + assert pid + for process in DeviceInfo(dvt).proclist(): + if pid == process['pid']: + assert process['name'] == 'MobileSafari' -def test_system_information(lockdown): +def test_system_information(dvt): """ Test getting system information. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ try: - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - system_info = DeviceInfo(dvt).system_information() + system_info = DeviceInfo(dvt).system_information() except UnrecognizedSelectorError: pytest.skip('device doesn\'t support this method') assert '_deviceDescription' in system_info and system_info['_deviceDescription'].startswith('Build Version') -def test_hardware_information(lockdown): +def test_hardware_information(dvt): """ Test getting hardware information. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - hardware_info = DeviceInfo(dvt).hardware_information() + hardware_info = DeviceInfo(dvt).hardware_information() assert hardware_info['numberOfCpus'] > 0 -def test_network_information(lockdown): +def test_network_information(dvt): """ Test getting network information. - :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with DvtSecureSocketProxyService(lockdown=lockdown) as dvt: - network_info = DeviceInfo(dvt).network_information() + network_info = DeviceInfo(dvt).network_information() assert network_info['lo0'] == 'Loopback' diff --git a/tests/services/test_screenshotr_relay.py b/tests/services/test_screenshotr_relay.py index 65af3ec31..c2928e547 100644 --- a/tests/services/test_screenshotr_relay.py +++ b/tests/services/test_screenshotr_relay.py @@ -1,14 +1,13 @@ -from pymobiledevice3.services.screenshot import ScreenshotService +from pymobiledevice3.services.dvt.instruments.screenshot import Screenshot PNG_HEADER = b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a' TIFF_HEADER = b'\x4D\x4D\x00\x2A' -def test_screenshot(lockdown): +def test_screenshot(dvt): """ Test that taking a screenshot returns a PNG. :param pymobiledevice3.lockdown.LockdownClient lockdown: Lockdown client. """ - with ScreenshotService(lockdown) as screenshot_taker: - screenshot = screenshot_taker.take_screenshot() - assert screenshot.startswith(PNG_HEADER) or screenshot.startswith(TIFF_HEADER) + screenshot = Screenshot(dvt).get_screenshot() + assert screenshot.startswith(PNG_HEADER) or screenshot.startswith(TIFF_HEADER) diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 4e10be162..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from pymobiledevice3.utils import sanitize_ios_version - - -@pytest.mark.parametrize('version, sanitized', [ - ('14.5', '14.5'), - ('14.5.1', '14.5'), - ('0.0', '0.0'), - ('9999.9999', '9999.9999'), - ('9999.9999.9999', '9999.9999'), -]) -def test_sanitize_ios_version(version, sanitized): - assert sanitize_ios_version(version) == sanitized From 23303503bfd949e02c3a724167fb1984f4f0ba0f Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 17 Oct 2023 13:50:35 +0300 Subject: [PATCH 02/13] tests: move dvt related tests into `instruments` subdir --- tests/services/{ => instruments}/test_dvt_secure_socket_proxy.py | 0 .../{test_screenshotr_relay.py => instruments/test_screenshot.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/services/{ => instruments}/test_dvt_secure_socket_proxy.py (100%) rename tests/services/{test_screenshotr_relay.py => instruments/test_screenshot.py} (100%) diff --git a/tests/services/test_dvt_secure_socket_proxy.py b/tests/services/instruments/test_dvt_secure_socket_proxy.py similarity index 100% rename from tests/services/test_dvt_secure_socket_proxy.py rename to tests/services/instruments/test_dvt_secure_socket_proxy.py diff --git a/tests/services/test_screenshotr_relay.py b/tests/services/instruments/test_screenshot.py similarity index 100% rename from tests/services/test_screenshotr_relay.py rename to tests/services/instruments/test_screenshot.py From cc36b6488e8fae902938bc48c7ec9ec457d6a62e Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 17 Oct 2023 13:51:27 +0300 Subject: [PATCH 03/13] remove: add a single simple `start_quic_tunnel()` command --- pymobiledevice3/cli/remote.py | 70 +++++++++---------- .../remote/core_device_tunnel_service.py | 17 +++++ 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/pymobiledevice3/cli/remote.py b/pymobiledevice3/cli/remote.py index 80927b4d3..008ed6680 100644 --- a/pymobiledevice3/cli/remote.py +++ b/pymobiledevice3/cli/remote.py @@ -3,21 +3,21 @@ from typing import List, TextIO import click -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 resume_remoted_if_required, stop_remoted, stop_remoted_if_required +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 + from pymobiledevice3.remote.core_device_tunnel_service import start_quic_tunnel except ImportError: + start_quic_tunnel = None logger.warning( - 'create_core_device_tunnel_service failed to be imported. Some feature may not work.\n' + 'start_quic_tunnel 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') @@ -68,38 +68,36 @@ def rsd_info(service_provider: RemoteServiceDiscoveryService, color: bool): print_json(service_provider.peer_info, colored=color) -async def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, secrets: TextIO, - script_mode: bool = False) -> None: - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +async def tunnel_task(service_provider: RemoteServiceDiscoveryService, secrets: TextIO, + script_mode: bool = False) -> None: + if start_quic_tunnel is None: + raise NotImplementedError('failed to start the QUIC tunnel on your platform') - stop_remoted_if_required() - with create_core_device_tunnel_service(service_provider, autopair=True) as service: - async with service.start_quic_tunnel(private_key, secrets_log_file=secrets) as tunnel_result: - resume_remoted_if_required() - if script_mode: - print(f'{tunnel_result.address} {tunnel_result.port}') - else: - 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') + - 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) + async with start_quic_tunnel(service_provider, secrets=secrets) as tunnel_result: + if script_mode: + print(f'{tunnel_result.address} {tunnel_result.port}') + else: + 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') + + 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') @@ -131,7 +129,7 @@ def cli_start_quic_tunnel(udid: str, secrets: TextIO, script_mode: bool): if udid is not None and rsd.udid != udid: raise NoDeviceConnectedError() - asyncio.run(start_quic_tunnel(rsd, secrets, script_mode=script_mode), debug=True) + asyncio.run(tunnel_task(rsd, secrets, script_mode), debug=True) @remote_cli.command('service', cls=RSDCommand) diff --git a/pymobiledevice3/remote/core_device_tunnel_service.py b/pymobiledevice3/remote/core_device_tunnel_service.py index c549ea382..8c4bef14e 100644 --- a/pymobiledevice3/remote/core_device_tunnel_service.py +++ b/pymobiledevice3/remote/core_device_tunnel_service.py @@ -20,6 +20,7 @@ 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 import rsa 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 @@ -42,6 +43,7 @@ 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.utils import resume_remoted_if_required, stop_remoted_if_required from pymobiledevice3.remote.xpc_message import XpcInt64Type, XpcUInt64Type from pymobiledevice3.utils import asyncio_print_traceback @@ -565,3 +567,18 @@ def create_core_device_tunnel_service(rsd: RemoteServiceDiscoveryService, autopa service = CoreDeviceTunnelService(rsd) service.connect(autopair=autopair) return service + + +@asynccontextmanager +async def start_quic_tunnel(service_provider: RemoteServiceDiscoveryService, secrets: Optional[TextIO] = None) \ + -> AsyncGenerator[TunnelResult, None]: + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + stop_remoted_if_required() + with create_core_device_tunnel_service(service_provider, autopair=True) as service: + async with service.start_quic_tunnel(private_key, secrets_log_file=secrets) as tunnel_result: + resume_remoted_if_required() + yield tunnel_result + + while True: + # wait user input while the asyncio tasks execute + await asyncio.sleep(.5) From 7d24f538dc9b74c84c78be83ad9622067f6aa9ed Mon Sep 17 00:00:00 2001 From: doronz Date: Tue, 17 Oct 2023 13:51:50 +0300 Subject: [PATCH 04/13] tests: add simulate-location tests --- tests/services/instruments/test_location.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/services/instruments/test_location.py diff --git a/tests/services/instruments/test_location.py b/tests/services/instruments/test_location.py new file mode 100644 index 000000000..f39533228 --- /dev/null +++ b/tests/services/instruments/test_location.py @@ -0,0 +1,17 @@ +from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation + + +def test_set_location(dvt): + """ + Test set location. + """ + # set to liberty island + LocationSimulation(dvt).simulate_location(40.690008, -74.045843) + + +def test_clear_location(dvt): + """ + Test clear location simulation + """ + # set to liberty island + LocationSimulation(dvt).stop() From ecd078832f0168ad00fbffd39b18123ffdfefd96 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 18 Oct 2023 08:14:35 +0300 Subject: [PATCH 05/13] crash_reports: improve sysdiagnose tarball detection --- pymobiledevice3/services/crash_reports.py | 34 +++++++++-------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index 40617323a..265c6d599 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -134,27 +134,19 @@ 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) not in SYSDIAGNOSE_PROCESS_NAMES) or \ - (posixpath.basename(syslog_entry.image_name) not in SYSDIAGNOSE_PROCESS_NAMES): - # filter only sysdianose lines - continue - - message = syslog_entry.message - - if message.startswith('SDArchive: Successfully created tar at '): - self.logger.info('sysdiagnose creation has begun') - for filename in self.ls('DiagnosticLogs/sysdiagnose'): - # search for an IN_PROGRESS archive - if 'IN_PROGRESS_' in filename: - 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' - break - break - + while sysdiagnose_filename is None: + self.afc.wait_exists('DiagnosticLogs/sysdiagnose') + for filename in self.ls('DiagnosticLogs/sysdiagnose'): + # search for an IN_PROGRESS archive + if 'IN_PROGRESS_' in filename: + 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' + break + break + self.logger.info('sysdiagnose tarball creation has been started') self.afc.wait_exists(sysdiagnose_filename) time.sleep(IOS17_SYSDIAGNOSE_DELAY) self.pull(out, entry=sysdiagnose_filename, erase=erase) From ff001d3dce8aacb9f7aebd69f23c901166509105 Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 18 Oct 2023 08:55:37 +0300 Subject: [PATCH 06/13] crash_reports: filter out old files when extracting sysdianose --- pymobiledevice3/services/crash_reports.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymobiledevice3/services/crash_reports.py b/pymobiledevice3/services/crash_reports.py index 265c6d599..52c907b5c 100644 --- a/pymobiledevice3/services/crash_reports.py +++ b/pymobiledevice3/services/crash_reports.py @@ -134,9 +134,17 @@ def get_new_sysdiagnose(self, out: str, erase: bool = True) -> None: """ sysdiagnose_filename = None + now = None + if isinstance(self.lockdown, LockdownClient): + now = self.lockdown.date + while sysdiagnose_filename is None: self.afc.wait_exists('DiagnosticLogs/sysdiagnose') for filename in self.ls('DiagnosticLogs/sysdiagnose'): + if now is not None and now.strftime('%Y.%m.%d') not in filename: + # filter out files that weren't created now + continue + # search for an IN_PROGRESS archive if 'IN_PROGRESS_' in filename: for ext in self.IN_PROGRESS_SYSDIAGNOSE_EXTENSIONS: From 9709a7208d0eb054c7c4082f9e4827bf5df714cb Mon Sep 17 00:00:00 2001 From: doronz Date: Wed, 18 Oct 2023 09:19:26 +0300 Subject: [PATCH 07/13] pyproject: bump version to 2.17.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a904a1267..21b1cf9d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.16.0" +version = "2.17.0" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 04502388de96f42f5a2352b1a46d35da44022c5e Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 19 Oct 2023 17:49:54 +0300 Subject: [PATCH 08/13] cli: refactor invalid service message --- pymobiledevice3/__main__.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index c337101ba..0e234159a 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -48,6 +48,21 @@ logger = logging.getLogger(__name__) +INVALID_SERVICE_MESSAGE = """Failed to start service. Possible reasons are: +- If you were trying to access a developer service (developer subcommand): + - Make sure the DeveloperDiskImage/PersonalizedImage is mounted via: + > python3 -m pymobiledevice3 mounter auto-mount + + - If you your device iOS version >= 17.0: + - Make sure you passed the --rsd option to the subcommand + https://github.com/doronz88/pymobiledevice3#working-with-developer-tools-ios--170 + +- Apple removed this service + +- A bug. Please file a bug report: + https://github.com/doronz88/pymobiledevice3/issues/new?assignees=&labels=&projects=&template=bug_report.md&title= +""" + def cli(): cli_commands = click.CommandCollection(sources=[ @@ -88,8 +103,7 @@ def cli(): logger.error('Developer Mode is disabled. You can try to enable it using: ' 'python3 -m pymobiledevice3 amfi enable-developer-mode') except InvalidServiceError: - logger.error('Failed to access an invalid lockdown service, possibly from DeveloperDiskImage.dmg or a Cryptex. ' - 'You may try: python3 -m pymobiledevice3 mounter auto-mount') + logger.error(INVALID_SERVICE_MESSAGE) except NoDeviceSelectedError: return except PasswordRequiredError: From bb79c8b713324deb373efc4fb119af6ec449f706 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 19 Oct 2023 18:30:05 +0300 Subject: [PATCH 09/13] cli: fix syslog's regex matching (`-e` and `-ei`) --- pymobiledevice3/cli/syslog.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pymobiledevice3/cli/syslog.py b/pymobiledevice3/cli/syslog.py index 50708a32f..62be05f33 100644 --- a/pymobiledevice3/cli/syslog.py +++ b/pymobiledevice3/cli/syslog.py @@ -91,12 +91,11 @@ def format_line(color, pid, syslog_entry, 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, - insensitive_regex): + include_label, regex, insensitive_regex): """ view live syslog lines """ - match_regex = [re.compile(f'.*({r}).*') for r in regex] - match_regex += [re.compile(f'.*({r}).*', re.IGNORECASE) for r in insensitive_regex] + match_regex = [re.compile(f'.*({r}).*', re.DOTALL) for r in regex] + match_regex += [re.compile(f'.*({r}).*', re.IGNORECASE | re.DOTALL) for r in insensitive_regex] def replace(m): if len(m.groups()): @@ -140,7 +139,6 @@ def replace(m): for r in match_regex: if not r.findall(line): continue - line = re.sub(r, replace, line) skip = False From 06e319453ec8a48b03c748a669a183aae5ac31aa Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 19 Oct 2023 18:35:35 +0300 Subject: [PATCH 10/13] cli: fix the invalid service message --- pymobiledevice3/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymobiledevice3/__main__.py b/pymobiledevice3/__main__.py index 0e234159a..b8c5c67da 100644 --- a/pymobiledevice3/__main__.py +++ b/pymobiledevice3/__main__.py @@ -53,7 +53,7 @@ - Make sure the DeveloperDiskImage/PersonalizedImage is mounted via: > python3 -m pymobiledevice3 mounter auto-mount - - If you your device iOS version >= 17.0: + - If your device iOS version >= 17.0: - Make sure you passed the --rsd option to the subcommand https://github.com/doronz88/pymobiledevice3#working-with-developer-tools-ios--170 From b16be4721018ae62aaf374c56a107caa2bd7a9e0 Mon Sep 17 00:00:00 2001 From: doronz Date: Thu, 19 Oct 2023 18:44:19 +0300 Subject: [PATCH 11/13] pyproject: bump version to 2.17.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 21b1cf9d3..5139f9582 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.17.0" +version = "2.17.1" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8" From 4c9e2066c367d2467605fd746e866d221d522c7c Mon Sep 17 00:00:00 2001 From: doronz88 Date: Thu, 19 Oct 2023 23:29:03 +0300 Subject: [PATCH 12/13] os_trace: fix `syslog collect` for windows (#614) --- pymobiledevice3/services/os_trace.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pymobiledevice3/services/os_trace.py b/pymobiledevice3/services/os_trace.py index 5cb9d9fa8..d1343aa2b 100644 --- a/pymobiledevice3/services/os_trace.py +++ b/pymobiledevice3/services/os_trace.py @@ -4,6 +4,7 @@ import tempfile import typing from datetime import datetime +from pathlib import Path from tarfile import TarFile from construct import Adapter, Byte, Bytes, Computed, Enum, Int16ul, Int32ul, Optional, RepeatUntil, Struct, this @@ -117,9 +118,11 @@ def collect(self, out: str, size_limit: int = None, age_limit: int = None, start """ Collect the system logs into a .logarchive that can be viewed later with tools such as log or Console. """ - with tempfile.NamedTemporaryFile() as tar: - self.create_archive(tar, size_limit=size_limit, age_limit=age_limit, start_time=start_time) - TarFile(tar.name).extractall(out) + with tempfile.TemporaryDirectory() as temp_dir: + file = Path(temp_dir) / 'foo.tar' + with open(file, 'wb') as f: + self.create_archive(f, size_limit=size_limit, age_limit=age_limit, start_time=start_time) + TarFile(file).extractall(out) def syslog(self, pid=-1): self.service.send_plist({'Request': 'StartActivity', 'MessageFilter': 65535, 'Pid': pid, 'StreamFlags': 60}) From 483a287a20b2546cbdd38957dcb1da7ce532429a Mon Sep 17 00:00:00 2001 From: DoronZ Date: Thu, 19 Oct 2023 23:44:12 +0300 Subject: [PATCH 13/13] pyproject: bump version to 2.17.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5139f9582..13530f4e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pymobiledevice3" -version = "2.17.1" +version = "2.17.2" description = "Pure python3 implementation for working with iDevices (iPhone, etc...)" readme = "README.md" requires-python = ">=3.8"