Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a738ccf

Browse files
committed
ios: Add installations subsystem
1 parent d7826b7 commit a738ccf

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

src/rpcclient/rpcclient/clients/ios/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from rpcclient.clients.ios.subsystems.accessibility import Accessibility
88
from rpcclient.clients.ios.subsystems.amfi import Amfi
99
from rpcclient.clients.ios.subsystems.backlight import Backlight
10+
from rpcclient.clients.ios.subsystems.installations import Installations
1011
from rpcclient.clients.ios.subsystems.lockdown import Lockdown
1112
from rpcclient.clients.ios.subsystems.mobile_gestalt import MobileGestalt
1213
from rpcclient.clients.ios.subsystems.processes import IosProcesses
@@ -65,6 +66,10 @@ def springboard(self) -> SpringBoard[DarwinSymbolT_co]:
6566
def amfi(self) -> Amfi[DarwinSymbolT_co]:
6667
return Amfi(self)
6768

69+
@subsystem
70+
def installations(self) -> Installations[DarwinSymbolT_co]:
71+
return Installations(self)
72+
6873
@zyncio.zmethod
6974
async def roots(self) -> list[str]:
7075
"""get a list of all accessible darwin roots when used for lookup of files/preferences/..."""
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import contextlib
2+
import os
3+
import plistlib
4+
import tempfile
5+
import zipfile
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING, Any, Generic
8+
9+
import zyncio
10+
11+
from rpcclient.clients.darwin._types import DarwinSymbolT_co
12+
from rpcclient.core._types import ClientBound
13+
from rpcclient.exceptions import BadReturnValueError
14+
from rpcclient.utils import assert_cast
15+
16+
17+
if TYPE_CHECKING:
18+
from rpcclient.clients.ios.client import BaseIosClient
19+
20+
21+
MOBILE_UID = 501
22+
MOBILE_GID = 501
23+
24+
25+
class Installations(ClientBound["BaseIosClient[DarwinSymbolT_co]"], Generic[DarwinSymbolT_co]):
26+
"""iOS application installation helpers.
27+
28+
This subsystem handles install flows that are not covered by the public
29+
MobileInstallation APIs exposed elsewhere. The methods operate through the
30+
connected RPC process and use remote filesystem, Objective-C, and
31+
LaunchServices calls on the target device.
32+
"""
33+
34+
def __init__(self, client: "BaseIosClient[DarwinSymbolT_co]") -> None:
35+
"""Bind the subsystem to an iOS client instance."""
36+
self._client = client
37+
38+
@zyncio.zmethod
39+
async def install_ipa(self, local_ipa: str | os.PathLike[str]) -> str:
40+
"""Install an IPA's app bundle.
41+
42+
The IPA is expanded locally, its `Payload/*.app` bundle is copied to a
43+
temporary directory on the device, and MobileContainerManager is used to
44+
create the app and data containers for the bundle identifier found in
45+
`Info.plist`. The staged app bundle is moved into its final app
46+
container, ownership and permissions are normalized for the `mobile`
47+
user, and LaunchServices is asked to register the app dictionary.
48+
49+
This method assumes the IPA is already signed in a form the target
50+
device can execute. It does not modify entitlements, signatures,
51+
provisioning profiles, or embedded binaries.
52+
53+
:param local_ipa: Local path to the IPA archive.
54+
:return: Final remote `.app` bundle path on the device.
55+
:raises RuntimeError: If the IPA has no app bundle, staging fails,
56+
container creation fails, or LaunchServices registration fails.
57+
"""
58+
local_ipa = Path(local_ipa).expanduser().resolve()
59+
60+
# Extract the IPA into a host-side temporary directory. IPA files are ZIP
61+
# archives and the installable bundle is expected under Payload/*.app.
62+
with tempfile.TemporaryDirectory(prefix="rpcipa-") as work_dir:
63+
work = Path(work_dir)
64+
with zipfile.ZipFile(local_ipa) as z:
65+
z.extractall(work)
66+
67+
# Locate the app bundle inside the extracted payload. Only the first
68+
# bundle is installed, matching the behavior of the original helper.
69+
payload = work / "Payload"
70+
apps = list(payload.glob("*.app"))
71+
if not apps:
72+
raise ValueError("IPA has no Payload/*.app")
73+
74+
local_app = apps[0]
75+
app_name = local_app.name
76+
77+
# Read the app metadata locally. The bundle identifier drives both
78+
# MobileContainerManager container creation and LaunchServices
79+
# registration.
80+
info = plistlib.loads((local_app / "Info.plist").read_bytes())
81+
bundle_id = info["CFBundleIdentifier"]
82+
83+
# Use a per-host-process remote staging directory under /var/tmp so
84+
# the app can be uploaded before it is moved into its container.
85+
remote_stage = f"/var/tmp/rpcinstall-{os.getpid()}"
86+
remote_app_stage = f"{remote_stage}/{app_name}"
87+
88+
try:
89+
# Create the remote staging directory and recursively upload the
90+
# extracted .app bundle into it.
91+
await self._client.fs.mkdir.z(remote_stage, parents=True, exist_ok=True)
92+
await self._client.fs.push.z(str(local_app), remote_stage, recursive=True, force=True)
93+
if not await self._client.fs.accessible.z(remote_app_stage, 0):
94+
raise RuntimeError(f"Failed to stage app bundle at {remote_app_stage}")
95+
96+
# Load the private frameworks that provide container management
97+
# and LaunchServices registration classes.
98+
await self._client.load_framework.z("MobileContainerManager")
99+
await self._client.load_framework.z("CoreServices")
100+
101+
# Resolve the Objective-C classes used for container allocation
102+
# and app registration.
103+
MCMAppContainer = await self._client.symbols.objc_getClass.z("MCMAppContainer")
104+
MCMAppDataContainer = await self._client.symbols.objc_getClass.z("MCMAppDataContainer")
105+
LSApplicationWorkspace = await self._client.symbols.objc_getClass.z("LSApplicationWorkspace")
106+
107+
# Create or fetch the application container for this bundle ID.
108+
# The resulting URL is where the .app bundle should live.
109+
app_container = await MCMAppContainer.objc_call.z(
110+
"containerWithIdentifier:createIfNecessary:existed:error:",
111+
await self._client.cf.z(bundle_id),
112+
True,
113+
0,
114+
0,
115+
)
116+
if not app_container:
117+
raise BadReturnValueError("Failed to create MCMAppContainer")
118+
119+
app_container_dir = assert_cast(
120+
str,
121+
await (await (await app_container.objc_call.z("url")).objc_call.z("path")).py.z(),
122+
)
123+
await self._client.fs.mkdir.z(app_container_dir, parents=True, exist_ok=True)
124+
final_app_path = f"{app_container_dir}/{app_name}"
125+
126+
# Replace any existing app bundle at the target path before the
127+
# staged bundle is moved into its final location.
128+
if await self._client.fs.accessible.z(final_app_path, 0):
129+
await self._client.fs.remove.z(final_app_path, recursive=True, force=True)
130+
131+
# Move the staged bundle into the app container and normalize
132+
# ownership and mode bits for iOS user-installed apps.
133+
await self._client.fs.rename.z(remote_app_stage, final_app_path)
134+
await self._client.fs.chown.z(final_app_path, MOBILE_UID, MOBILE_GID, recursive=True)
135+
await self._client.fs.chmod.z(final_app_path, 0o755, recursive=True)
136+
137+
# Create or fetch the application data container. If this works,
138+
# the registration dictionary will point HOME and TMPDIR into it.
139+
data_container = await MCMAppDataContainer.objc_call.z(
140+
"containerWithIdentifier:createIfNecessary:existed:error:",
141+
await self._client.cf.z(bundle_id),
142+
True,
143+
0,
144+
0,
145+
)
146+
147+
data_container_dir = ""
148+
if data_container:
149+
data_container_dir = assert_cast(
150+
str,
151+
await (await (await data_container.objc_call.z("url")).objc_call.z("path")).py.z(),
152+
)
153+
154+
# Build the LaunchServices registration dictionary. These keys
155+
# describe the user app, its final path, signing metadata, and
156+
# install state closely enough for LS to make it launchable.
157+
reg: dict[str, Any] = {
158+
"ApplicationType": "User",
159+
"CFBundleIdentifier": bundle_id,
160+
"CodeInfoIdentifier": bundle_id,
161+
"CompatibilityState": 0,
162+
"IsContainerized": True,
163+
"IsDeletable": True,
164+
"Path": final_app_path,
165+
"SignerOrganization": "Apple Inc.",
166+
"SignatureVersion": 132352,
167+
"SignerIdentity": "Apple iPhone OS Application Signing",
168+
"IsAdHocSigned": True,
169+
"LSInstallType": 1,
170+
"HasMIDBasedSINF": False,
171+
"MissingSINF": False,
172+
"FamilyID": 0,
173+
"IsOnDemandInstallCapable": False,
174+
"_LSBundlePlugins": {},
175+
}
176+
177+
# Include container-specific environment variables when the data
178+
# container exists, matching what user apps expect at launch.
179+
if data_container_dir:
180+
reg["Container"] = data_container_dir
181+
reg["EnvironmentVariables"] = {
182+
"CFFIXED_USER_HOME": data_container_dir,
183+
"HOME": data_container_dir,
184+
"TMPDIR": f"{data_container_dir}/tmp",
185+
}
186+
187+
# Ask LaunchServices to register the app. A false return means
188+
# the app bundle exists on disk but was not made launchable.
189+
workspace = await LSApplicationWorkspace.objc_call.z("defaultWorkspace")
190+
ok = await workspace.objc_call.z("registerApplicationDictionary:", await self._client.cf.z(reg))
191+
if not ok:
192+
raise BadReturnValueError("LaunchServices registration failed")
193+
194+
return final_app_path
195+
finally:
196+
# Always remove the temporary staging directory. Failures during
197+
# cleanup are suppressed, so the original installation error is kept.
198+
with contextlib.suppress(Exception):
199+
if await self._client.fs.accessible.z(remote_stage, 0):
200+
await self._client.fs.remove.z(remote_stage, recursive=True, force=True)

0 commit comments

Comments
 (0)