mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Merge branch 'master' into rppairing
This commit is contained in:
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -30,8 +30,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install rustup targets
|
- name: Install rustup targets
|
||||||
run: |
|
run: |
|
||||||
rustup target add aarch64-apple-ios && rustup target add x86_64-apple-ios && \
|
rustup target add aarch64-apple-darwin && \
|
||||||
rustup target add aarch64-apple-ios-sim && rustup target add aarch64-apple-darwin && \
|
|
||||||
rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli
|
rustup target add x86_64-apple-darwin && cargo install --force --locked bindgen-cli
|
||||||
|
|
||||||
- name: Build all Apple targets and examples/tools
|
- name: Build all Apple targets and examples/tools
|
||||||
@@ -45,12 +44,6 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
target/*apple*/release/libidevice_ffi.a
|
target/*apple*/release/libidevice_ffi.a
|
||||||
|
|
||||||
- name: Upload macOS+iOS XCFramework
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: idevice-xcframework
|
|
||||||
path: swift/bundle.zip
|
|
||||||
|
|
||||||
- name: Upload C examples/tools
|
- name: Upload C examples/tools
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
72
Cargo.lock
generated
72
Cargo.lock
generated
@@ -119,6 +119,19 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-compression"
|
||||||
|
version = "0.4.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@@ -147,6 +160,21 @@ version = "4.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async_zip"
|
||||||
|
version = "0.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
|
||||||
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
|
"crc32fast",
|
||||||
|
"futures-lite",
|
||||||
|
"pin-project",
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -916,7 +944,10 @@ version = "2.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1255,9 +1286,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idevice"
|
name = "idevice"
|
||||||
version = "0.1.41"
|
version = "0.1.42"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
"async_zip",
|
||||||
"base64",
|
"base64",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1868,6 +1900,26 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -1927,9 +1979,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plist_ffi"
|
name = "plist_ffi"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22a5ca928241bc2e8c5fd28b81772962389efdbfcb71dfc9ec694369e063cb3a"
|
checksum = "35ed070b06d9f2fdd7e816ef784fb07b09672f2acf37527f810dbedf450b7769"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cbindgen",
|
"cbindgen",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -2713,6 +2765,20 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.23"
|
version = "0.8.23"
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -1,13 +1,16 @@
|
|||||||
# idevice
|
# idevice
|
||||||
|
|
||||||
A Rust library for interacting with iOS services.
|
A pure Rust library for interacting with iOS services.
|
||||||
Inspired by [libimobiledevice](https://github.com/libimobiledevice/libimobiledevice)
|
Inspired by [libimobiledevice](https://github.com/libimobiledevice/libimobiledevice)
|
||||||
and [pymobiledevice3](https://github.com/doronz88/pymobiledevice3),
|
[pymobiledevice3](https://github.com/doronz88/pymobiledevice3),
|
||||||
this library interfaces with lockdownd and usbmuxd to perform actions
|
and [go-ios](https://github.com/danielpaulus/go-ios)
|
||||||
|
this library interfaces with lockdownd, usbmuxd, and RSD to perform actions
|
||||||
on an iOS device that a Mac normally would.
|
on an iOS device that a Mac normally would.
|
||||||
|
|
||||||
For help and information, join the [idevice Discord](https://discord.gg/qtgv6QtYbV)
|
For help and information, join the [idevice Discord](https://discord.gg/qtgv6QtYbV)
|
||||||
|
|
||||||
|
[](https://deepwiki.com/jkcoxson/idevice)
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
**IMPORTANT**: Breaking changes will happen at each point release until 0.2.0.
|
**IMPORTANT**: Breaking changes will happen at each point release until 0.2.0.
|
||||||
@@ -17,6 +20,25 @@ This library is in development and research stage.
|
|||||||
Releases are being published to crates.io for use in other projects,
|
Releases are being published to crates.io for use in other projects,
|
||||||
but the API and feature-set are far from final or even planned.
|
but the API and feature-set are far from final or even planned.
|
||||||
|
|
||||||
|
## Why use this?
|
||||||
|
|
||||||
|
libimobiledevice is a groundbreaking library. Unfortunately, it hasn't
|
||||||
|
been seriously updated in a long time, and does not support many modern
|
||||||
|
iOS features.
|
||||||
|
|
||||||
|
Libraries such as pymobiledevice3 and go-ios have popped up to fill that
|
||||||
|
gap, but both lacked the support I needed for embedding into applications
|
||||||
|
and server programs. Python requires an interpreter, and Go's current
|
||||||
|
ability to be embedded in other languages is lacking.
|
||||||
|
|
||||||
|
This library is currently used in popular apps such as
|
||||||
|
[StikDebug](https://github.com/StephenDev0/StikDebug),
|
||||||
|
[CrossCode](https://github.com/nab138/CrossCode)
|
||||||
|
and
|
||||||
|
[Protokolle](https://github.com/khcrysalis/Protokolle).
|
||||||
|
``idevice`` has proven there is a need. It's currently deployed on tens of
|
||||||
|
thousands of devices, all across the world.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
To keep dependency bloat and compile time down, everything is contained in features.
|
To keep dependency bloat and compile time down, everything is contained in features.
|
||||||
@@ -81,7 +103,7 @@ async fn main() {
|
|||||||
// We'll ask usbmuxd for a device
|
// We'll ask usbmuxd for a device
|
||||||
let mut usbmuxd = UsbmuxdConnection::default()
|
let mut usbmuxd = UsbmuxdConnection::default()
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to usbmxud")
|
.expect("Unable to connect to usbmuxd");
|
||||||
let devs = usbmuxd.get_devices().unwrap();
|
let devs = usbmuxd.get_devices().unwrap();
|
||||||
if devs.is_empty() {
|
if devs.is_empty() {
|
||||||
eprintln!("No devices connected!");
|
eprintln!("No devices connected!");
|
||||||
@@ -118,23 +140,43 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
More examples are in the ``tools`` crate and in the crate documentation.
|
More examples are in the [`tools`](tools/) crate and in the crate documentation.
|
||||||
|
|
||||||
## FFI
|
## FFI
|
||||||
|
|
||||||
For use in other languages, a small FFI crate has been created to start exposing
|
For use in other languages, a small FFI crate has been created to start exposing
|
||||||
idevice. Example C programs can be found in this repository.
|
idevice. Example C programs can be found in the [`ffi/examples`](ffi/examples/) directory.
|
||||||
|
|
||||||
## Version Policy
|
### C++
|
||||||
|
|
||||||
As Apple prohibits downgrading to older versions, this library will
|
"Hey wait a second, there's a lot of C++ code in this library!!"
|
||||||
not keep compatibility for older versions than the current stable release.
|
C++ bindings have been made for many of idevice's features. This allows smooth
|
||||||
|
and safer usage in C++ and Swift codebases.
|
||||||
|
|
||||||
## Developer Disk Images
|
## Technical Explanation
|
||||||
|
|
||||||
doronz88 is kind enough to maintain a [repo](https://github.com/doronz88/DeveloperDiskImage)
|
There are so many layers and protocols in this library, many stacked on top of
|
||||||
for disk images and personalized images.
|
one another. It's difficult to describe the magnitude that is Apple's interfaces.
|
||||||
On MacOS, you can find them at ``~/Library/Developer/DeveloperDiskImages``.
|
|
||||||
|
I would recommend reading the DeepWiki explanations and overviews to get an idea
|
||||||
|
of how this library and their associated protocols work. But a general overview is:
|
||||||
|
|
||||||
|
### Lockdown
|
||||||
|
|
||||||
|
1. A lockdown service is accessible via a port given by lockdown
|
||||||
|
1. Lockdown is accessible by USB or TCP via TLS
|
||||||
|
1. USB is accessible via usbmuxd
|
||||||
|
1. usbmuxd is accessed through a unix socket
|
||||||
|
1. That Unix socket has its own protocol
|
||||||
|
|
||||||
|
### RemoteXPC/RSD
|
||||||
|
|
||||||
|
1. An RSD service is discovered through a RemoteXPC handshake response
|
||||||
|
1. RemoteXPC is transferred over non-compliant HTTP/2
|
||||||
|
1. That HTTP/2 is accessed through an NCM USB interface or CoreDeviceProxy
|
||||||
|
1. CoreDeviceProxy is a lockdown service, see above
|
||||||
|
|
||||||
|
This doesn't even touch RPPairing, which is still a mystery as of writing.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
230
cpp/examples/mounter.cpp
Normal file
230
cpp/examples/mounter.cpp
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Idevice++ library headers
|
||||||
|
#include <idevice++/lockdown.hpp>
|
||||||
|
#include <idevice++/mobile_image_mounter.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <idevice++/usbmuxd.hpp>
|
||||||
|
#include <plist/plist++.h>
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads an entire file into a byte vector.
|
||||||
|
* @param path The path to the file.
|
||||||
|
* @return A vector containing the file's data.
|
||||||
|
*/
|
||||||
|
std::vector<uint8_t> read_file(const std::string& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to open file: " + path);
|
||||||
|
}
|
||||||
|
std::streamsize size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
std::vector<uint8_t> buffer(size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
|
||||||
|
throw std::runtime_error("Failed to read file: " + path);
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Prints the command usage instructions.
|
||||||
|
*/
|
||||||
|
void print_usage(const char* prog_name) {
|
||||||
|
std::cerr << "Usage: " << prog_name << " [options] <subcommand>\n\n"
|
||||||
|
<< "A tool to manage developer images on a device.\n\n"
|
||||||
|
<< "Options:\n"
|
||||||
|
<< " --udid <UDID> Target a specific device by its UDID.\n\n"
|
||||||
|
<< "Subcommands:\n"
|
||||||
|
<< " list List mounted images.\n"
|
||||||
|
<< " unmount Unmount the developer image.\n"
|
||||||
|
<< " mount [mount_options] Mount a developer image.\n\n"
|
||||||
|
<< "Mount Options:\n"
|
||||||
|
<< " --image <path> (Required) Path to the DeveloperDiskImage.dmg.\n"
|
||||||
|
<< " --signature <path> (Required for iOS < 17) Path to the .signature file.\n"
|
||||||
|
<< " --manifest <path> (Required for iOS 17+) Path to the BuildManifest.plist.\n"
|
||||||
|
<< " --trustcache <path> (Required for iOS 17+) Path to the trust cache file.\n"
|
||||||
|
<< std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Logic ---
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
idevice_init_logger(Debug, Disabled, NULL);
|
||||||
|
// --- 1. Argument Parsing ---
|
||||||
|
if (argc < 2) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string udid_arg;
|
||||||
|
std::string subcommand;
|
||||||
|
std::string image_path;
|
||||||
|
std::string signature_path;
|
||||||
|
std::string manifest_path;
|
||||||
|
std::string trustcache_path;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
std::string arg = argv[i];
|
||||||
|
if (arg == "--udid" && i + 1 < argc) {
|
||||||
|
udid_arg = argv[++i];
|
||||||
|
} else if (arg == "--image" && i + 1 < argc) {
|
||||||
|
image_path = argv[++i];
|
||||||
|
} else if (arg == "--signature" && i + 1 < argc) {
|
||||||
|
signature_path = argv[++i];
|
||||||
|
} else if (arg == "--manifest" && i + 1 < argc) {
|
||||||
|
manifest_path = argv[++i];
|
||||||
|
} else if (arg == "--trustcache" && i + 1 < argc) {
|
||||||
|
trustcache_path = argv[++i];
|
||||||
|
} else if (arg == "list" || arg == "mount" || arg == "unmount") {
|
||||||
|
subcommand = arg;
|
||||||
|
} else if (arg == "--help" || arg == "-h") {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand.empty()) {
|
||||||
|
std::cerr << "Error: No subcommand specified. Use 'list', 'mount', or 'unmount'."
|
||||||
|
<< std::endl;
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- 2. Device Connection ---
|
||||||
|
auto u =
|
||||||
|
IdeviceFFI::UsbmuxdConnection::default_new(0).expect("Failed to connect to usbmuxd");
|
||||||
|
auto devices = u.get_devices().expect("Failed to get devices from usbmuxd");
|
||||||
|
if (devices.empty()) {
|
||||||
|
throw std::runtime_error("No devices connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IdeviceFFI::UsbmuxdDevice* target_dev = nullptr;
|
||||||
|
if (!udid_arg.empty()) {
|
||||||
|
for (auto& dev : devices) {
|
||||||
|
if (dev.get_udid().unwrap_or("") == udid_arg) {
|
||||||
|
target_dev = &dev;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!target_dev) {
|
||||||
|
throw std::runtime_error("Device with UDID " + udid_arg + " not found.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target_dev = &devices[0]; // Default to the first device
|
||||||
|
}
|
||||||
|
|
||||||
|
auto udid = target_dev->get_udid().expect("Device has no UDID");
|
||||||
|
auto id = target_dev->get_id().expect("Device has no ID");
|
||||||
|
|
||||||
|
IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new();
|
||||||
|
auto prov = IdeviceFFI::Provider::usbmuxd_new(std::move(addr), 0, udid, id, "mounter-tool")
|
||||||
|
.expect("Failed to create provider");
|
||||||
|
|
||||||
|
// --- 3. Connect to Lockdown & Get iOS Version ---
|
||||||
|
auto lockdown_client =
|
||||||
|
IdeviceFFI::Lockdown::connect(prov).expect("Lockdown connect failed");
|
||||||
|
|
||||||
|
auto pairing_file = prov.get_pairing_file().expect("Failed to get pairing file");
|
||||||
|
lockdown_client.start_session(pairing_file).expect("Failed to start session");
|
||||||
|
|
||||||
|
auto version_plist = lockdown_client.get_value("ProductVersion", NULL)
|
||||||
|
.expect("Failed to get ProductVersion");
|
||||||
|
PList::String version_node(version_plist);
|
||||||
|
std::string version_str = version_node.GetValue();
|
||||||
|
std::cout << "Version string: " << version_str << std::endl;
|
||||||
|
|
||||||
|
if (version_str.empty()) {
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Failed to get a valid ProductVersion string from the device.");
|
||||||
|
}
|
||||||
|
int major_version = std::stoi(version_str);
|
||||||
|
|
||||||
|
// --- 4. Connect to MobileImageMounter ---
|
||||||
|
auto mounter_client = IdeviceFFI::MobileImageMounter::connect(prov).expect(
|
||||||
|
"Failed to connect to image mounter");
|
||||||
|
|
||||||
|
// --- 5. Execute Subcommand ---
|
||||||
|
if (subcommand == "list") {
|
||||||
|
auto images = mounter_client.copy_devices().expect("Failed to get images");
|
||||||
|
std::cout << "Mounted Images:\n";
|
||||||
|
for (plist_t p : images) {
|
||||||
|
PList::Dictionary dict(p);
|
||||||
|
std::cout << dict.ToXml() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (subcommand == "unmount") {
|
||||||
|
const char* unmount_path = (major_version < 17) ? "/Developer" : "/System/Developer";
|
||||||
|
mounter_client.unmount_image(unmount_path).expect("Failed to unmount image");
|
||||||
|
std::cout << "Successfully unmounted image from " << unmount_path << std::endl;
|
||||||
|
|
||||||
|
} else if (subcommand == "mount") {
|
||||||
|
if (image_path.empty()) {
|
||||||
|
throw std::runtime_error("Mount command requires --image <path>");
|
||||||
|
}
|
||||||
|
auto image_data = read_file(image_path);
|
||||||
|
|
||||||
|
if (major_version < 17) {
|
||||||
|
if (signature_path.empty()) {
|
||||||
|
throw std::runtime_error("iOS < 17 requires --signature <path>");
|
||||||
|
}
|
||||||
|
auto signature_data = read_file(signature_path);
|
||||||
|
mounter_client
|
||||||
|
.mount_developer(image_data.data(),
|
||||||
|
image_data.size(),
|
||||||
|
signature_data.data(),
|
||||||
|
signature_data.size())
|
||||||
|
.expect("Failed to mount developer image");
|
||||||
|
} else { // iOS 17+
|
||||||
|
if (manifest_path.empty() || trustcache_path.empty()) {
|
||||||
|
throw std::runtime_error("iOS 17+ requires --manifest and --trustcache paths");
|
||||||
|
}
|
||||||
|
auto manifest_data = read_file(manifest_path);
|
||||||
|
auto trustcache_data = read_file(trustcache_path);
|
||||||
|
|
||||||
|
auto chip_id_plist = lockdown_client.get_value(nullptr, "UniqueChipID")
|
||||||
|
.expect("Failed to get UniqueChipID");
|
||||||
|
PList::Integer chip_id_node(chip_id_plist);
|
||||||
|
uint64_t unique_chip_id = chip_id_node.GetValue();
|
||||||
|
|
||||||
|
std::function<void(size_t, size_t)> progress_callback = [](size_t n, size_t d) {
|
||||||
|
if (d == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double percent = (static_cast<double>(n) / d) * 100.0;
|
||||||
|
std::cout << "\rProgress: " << std::fixed << std::setprecision(2) << percent
|
||||||
|
<< "%" << std::flush;
|
||||||
|
if (n == d) {
|
||||||
|
std::cout << std::endl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mounter_client
|
||||||
|
.mount_personalized_with_callback(prov,
|
||||||
|
image_data.data(),
|
||||||
|
image_data.size(),
|
||||||
|
trustcache_data.data(),
|
||||||
|
trustcache_data.size(),
|
||||||
|
manifest_data.data(),
|
||||||
|
manifest_data.size(),
|
||||||
|
nullptr, // info_plist
|
||||||
|
unique_chip_id,
|
||||||
|
progress_callback)
|
||||||
|
.expect("Failed to mount personalized image");
|
||||||
|
}
|
||||||
|
std::cout << "Successfully mounted image." << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "Error: " << e.what() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
40
cpp/include/idevice++/heartbeat.hpp
Normal file
40
cpp/include/idevice++/heartbeat.hpp
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <sys/_types/_u_int64_t.h>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using HeartbeatPtr =
|
||||||
|
std::unique_ptr<HeartbeatClientHandle, FnDeleter<HeartbeatClientHandle, heartbeat_client_free>>;
|
||||||
|
|
||||||
|
class Heartbeat {
|
||||||
|
public:
|
||||||
|
// Factory: connect via Provider
|
||||||
|
static Result<Heartbeat, FfiError> connect(Provider& provider);
|
||||||
|
|
||||||
|
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||||
|
static Result<Heartbeat, FfiError> from_socket(Idevice&& socket);
|
||||||
|
|
||||||
|
// Ops
|
||||||
|
Result<void, FfiError> send_polo();
|
||||||
|
Result<u_int64_t, FfiError> get_marco(u_int64_t interval);
|
||||||
|
|
||||||
|
// RAII / moves
|
||||||
|
~Heartbeat() noexcept = default;
|
||||||
|
Heartbeat(Heartbeat&&) noexcept = default;
|
||||||
|
Heartbeat& operator=(Heartbeat&&) noexcept = default;
|
||||||
|
Heartbeat(const Heartbeat&) = delete;
|
||||||
|
Heartbeat& operator=(const Heartbeat&) = delete;
|
||||||
|
|
||||||
|
HeartbeatClientHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static Heartbeat adopt(HeartbeatClientHandle* h) noexcept { return Heartbeat(h); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Heartbeat(HeartbeatClientHandle* h) noexcept : handle_(h) {}
|
||||||
|
HeartbeatPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
60
cpp/include/idevice++/installation_proxy.hpp
Normal file
60
cpp/include/idevice++/installation_proxy.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <sys/_types/_u_int64_t.h>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using InstallationProxyPtr =
|
||||||
|
std::unique_ptr<InstallationProxyClientHandle,
|
||||||
|
FnDeleter<InstallationProxyClientHandle, installation_proxy_client_free>>;
|
||||||
|
|
||||||
|
class InstallationProxy {
|
||||||
|
public:
|
||||||
|
// Factory: connect via Provider
|
||||||
|
static Result<InstallationProxy, FfiError> connect(Provider& provider);
|
||||||
|
|
||||||
|
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||||
|
static Result<InstallationProxy, FfiError> from_socket(Idevice&& socket);
|
||||||
|
|
||||||
|
// Ops
|
||||||
|
Result<std::vector<plist_t>, FfiError>
|
||||||
|
get_apps(Option<std::string> application_type,
|
||||||
|
Option<std::vector<std::string>> bundle_identifiers);
|
||||||
|
Result<void, FfiError> install(std::string package_path, Option<plist_t> options);
|
||||||
|
Result<void, FfiError> install_with_callback(std::string package_path,
|
||||||
|
Option<plist_t> options,
|
||||||
|
std::function<void(u_int64_t)>& lambda);
|
||||||
|
Result<void, FfiError> upgrade(std::string package_path, Option<plist_t> options);
|
||||||
|
Result<void, FfiError> upgrade_with_callback(std::string package_path,
|
||||||
|
Option<plist_t> options,
|
||||||
|
std::function<void(u_int64_t)>& lambda);
|
||||||
|
Result<void, FfiError> uninstall(std::string package_path, Option<plist_t> options);
|
||||||
|
Result<void, FfiError> uninstall_with_callback(std::string package_path,
|
||||||
|
Option<plist_t> options,
|
||||||
|
std::function<void(u_int64_t)>& lambda);
|
||||||
|
Result<bool, FfiError> check_capabilities_match(std::vector<plist_t> capabilities,
|
||||||
|
Option<plist_t> options);
|
||||||
|
Result<std::vector<plist_t>, FfiError> browse(Option<plist_t> options);
|
||||||
|
|
||||||
|
// RAII / moves
|
||||||
|
~InstallationProxy() noexcept = default;
|
||||||
|
InstallationProxy(InstallationProxy&&) noexcept = default;
|
||||||
|
InstallationProxy& operator=(InstallationProxy&&) noexcept = default;
|
||||||
|
InstallationProxy(const InstallationProxy&) = delete;
|
||||||
|
InstallationProxy& operator=(const InstallationProxy&) = delete;
|
||||||
|
|
||||||
|
InstallationProxyClientHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static InstallationProxy adopt(InstallationProxyClientHandle* h) noexcept {
|
||||||
|
return InstallationProxy(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit InstallationProxy(InstallationProxyClientHandle* h) noexcept : handle_(h) {}
|
||||||
|
InstallationProxyPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <idevice++/bindings.hpp>
|
#include <idevice++/bindings.hpp>
|
||||||
|
|||||||
87
cpp/include/idevice++/mobile_image_mounter.hpp
Normal file
87
cpp/include/idevice++/mobile_image_mounter.hpp
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using MobileImageMounterPtr =
|
||||||
|
std::unique_ptr<ImageMounterHandle, FnDeleter<ImageMounterHandle, image_mounter_free>>;
|
||||||
|
|
||||||
|
class MobileImageMounter {
|
||||||
|
public:
|
||||||
|
// Factory: connect via Provider
|
||||||
|
static Result<MobileImageMounter, FfiError> connect(Provider& provider);
|
||||||
|
|
||||||
|
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||||
|
static Result<MobileImageMounter, FfiError> from_socket(Idevice&& socket);
|
||||||
|
|
||||||
|
// Ops
|
||||||
|
Result<std::vector<plist_t>, FfiError> copy_devices();
|
||||||
|
Result<std::vector<uint8_t>, FfiError> lookup_image(std::string image_type);
|
||||||
|
Result<void, FfiError> upload_image(std::string image_type,
|
||||||
|
const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* signature_data,
|
||||||
|
size_t signature_size);
|
||||||
|
Result<void, FfiError> mount_image(std::string image_type,
|
||||||
|
const uint8_t* signature_data,
|
||||||
|
size_t signature_size,
|
||||||
|
const uint8_t* trust_cache_data,
|
||||||
|
size_t trust_cache_size,
|
||||||
|
plist_t info_plist);
|
||||||
|
Result<void, FfiError> unmount_image(std::string mount_path);
|
||||||
|
Result<bool, FfiError> query_developer_mode_status();
|
||||||
|
Result<void, FfiError> mount_developer(const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* signature_data,
|
||||||
|
size_t signature_size);
|
||||||
|
Result<std::vector<uint8_t>, FfiError> query_personalization_manifest(
|
||||||
|
std::string image_type, const uint8_t* signature_data, size_t signature_size);
|
||||||
|
Result<std::vector<uint8_t>, FfiError> query_nonce(std::string personalized_image_type);
|
||||||
|
Result<plist_t, FfiError> query_personalization_identifiers(std::string image_type);
|
||||||
|
Result<void, FfiError> roll_personalization_nonce();
|
||||||
|
Result<void, FfiError> roll_cryptex_nonce();
|
||||||
|
Result<void, FfiError> mount_personalized(Provider& provider,
|
||||||
|
const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* trust_cache_data,
|
||||||
|
size_t trust_cache_size,
|
||||||
|
const uint8_t* build_manifest_data,
|
||||||
|
size_t build_manifest_size,
|
||||||
|
plist_t info_plist,
|
||||||
|
uint64_t unique_chip_id);
|
||||||
|
Result<void, FfiError>
|
||||||
|
mount_personalized_with_callback(Provider& provider,
|
||||||
|
const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* trust_cache_data,
|
||||||
|
size_t trust_cache_size,
|
||||||
|
const uint8_t* build_manifest_data,
|
||||||
|
size_t build_manifest_size,
|
||||||
|
plist_t info_plist,
|
||||||
|
uint64_t unique_chip_id,
|
||||||
|
std::function<void(size_t, size_t)>& lambda);
|
||||||
|
|
||||||
|
// RAII / moves
|
||||||
|
~MobileImageMounter() noexcept = default;
|
||||||
|
MobileImageMounter(MobileImageMounter&&) noexcept = default;
|
||||||
|
MobileImageMounter& operator=(MobileImageMounter&&) noexcept = default;
|
||||||
|
MobileImageMounter(const MobileImageMounter&) = delete;
|
||||||
|
MobileImageMounter& operator=(const MobileImageMounter&) = delete;
|
||||||
|
|
||||||
|
ImageMounterHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static MobileImageMounter adopt(ImageMounterHandle* h) noexcept {
|
||||||
|
return MobileImageMounter(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit MobileImageMounter(ImageMounterHandle* h) noexcept : handle_(h) {}
|
||||||
|
MobileImageMounterPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -115,6 +116,34 @@ template <typename T> class Option {
|
|||||||
return has_ ? std::move(*ptr()) : static_cast<T>(f());
|
return has_ ? std::move(*ptr()) : static_cast<T>(f());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T expect(const char* message) && {
|
||||||
|
if (is_none()) {
|
||||||
|
std::fprintf(stderr, "Fatal (expect) error: %s\n", message);
|
||||||
|
std::terminate();
|
||||||
|
}
|
||||||
|
T tmp = std::move(*ptr());
|
||||||
|
reset();
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a mutable reference from an lvalue Result
|
||||||
|
T& expect(const char* message) & {
|
||||||
|
if (is_none()) {
|
||||||
|
std::fprintf(stderr, "Fatal (expect) error: %s\n", message);
|
||||||
|
std::terminate();
|
||||||
|
}
|
||||||
|
return *ptr();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a const reference from a const lvalue Result
|
||||||
|
const T& expect(const char* message) const& {
|
||||||
|
if (is_none()) {
|
||||||
|
std::fprintf(stderr, "Fatal (expect) error: %s\n", message);
|
||||||
|
std::terminate();
|
||||||
|
}
|
||||||
|
return *ptr();
|
||||||
|
}
|
||||||
|
|
||||||
// map
|
// map
|
||||||
template <typename F>
|
template <typename F>
|
||||||
auto map(F&& f) const& -> Option<typename std::decay<decltype(f(*ptr()))>::type> {
|
auto map(F&& f) const& -> Option<typename std::decay<decltype(f(*ptr()))>::type> {
|
||||||
|
|||||||
41
cpp/include/idevice++/process_control.hpp
Normal file
41
cpp/include/idevice++/process_control.hpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/remote_server.hpp>
|
||||||
|
#include <idevice++/result.hpp>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using ProcessControlPtr =
|
||||||
|
std::unique_ptr<ProcessControlHandle, FnDeleter<ProcessControlHandle, process_control_free>>;
|
||||||
|
|
||||||
|
class ProcessControl {
|
||||||
|
public:
|
||||||
|
// Factory: borrows the RemoteServer; not consumed
|
||||||
|
static Result<ProcessControl, FfiError> create(RemoteServer& server);
|
||||||
|
|
||||||
|
Result<u_int64_t, FfiError> launch_app(std::string bundle_id,
|
||||||
|
Option<std::vector<std::string>> env_vars,
|
||||||
|
Option<std::vector<std::string>> arguments,
|
||||||
|
bool start_suspended,
|
||||||
|
bool kill_existing);
|
||||||
|
Result<void, FfiError> kill_app(u_int64_t pid);
|
||||||
|
Result<void, FfiError> disable_memory_limit(u_int64_t pid);
|
||||||
|
|
||||||
|
~ProcessControl() noexcept = default;
|
||||||
|
ProcessControl(ProcessControl&&) noexcept = default;
|
||||||
|
ProcessControl& operator=(ProcessControl&&) noexcept = default;
|
||||||
|
ProcessControl(const ProcessControl&) = delete;
|
||||||
|
ProcessControl& operator=(const ProcessControl&) = delete;
|
||||||
|
|
||||||
|
ProcessControlHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static ProcessControl adopt(ProcessControlHandle* h) noexcept { return ProcessControl(h); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit ProcessControl(ProcessControlHandle* h) noexcept : handle_(h) {}
|
||||||
|
ProcessControlPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
47
cpp/src/heartbeat.cpp
Normal file
47
cpp/src/heartbeat.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/heartbeat.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
Result<Heartbeat, FfiError> Heartbeat::connect(Provider& provider) {
|
||||||
|
HeartbeatClientHandle* out = nullptr;
|
||||||
|
FfiError e(::heartbeat_connect(provider.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
provider.release();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(Heartbeat::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Heartbeat, FfiError> Heartbeat::from_socket(Idevice&& socket) {
|
||||||
|
HeartbeatClientHandle* out = nullptr;
|
||||||
|
FfiError e(::heartbeat_new(socket.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
socket.release();
|
||||||
|
return Ok(Heartbeat::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> Heartbeat::send_polo() {
|
||||||
|
FfiError e(::heartbeat_send_polo(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<u_int64_t, FfiError> Heartbeat::get_marco(u_int64_t interval) {
|
||||||
|
u_int64_t new_interval = 0;
|
||||||
|
FfiError e(::heartbeat_get_marco(handle_.get(), interval, &new_interval));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(new_interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
240
cpp/src/installation_proxy.cpp
Normal file
240
cpp/src/installation_proxy.cpp
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/installation_proxy.hpp>
|
||||||
|
#include <sys/_types/_u_int64_t.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
// -------- Anonymous Namespace for Helpers --------
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
* @brief A C-style trampoline function to call back into a C++ std::function.
|
||||||
|
*
|
||||||
|
* This function is passed to the Rust FFI layer. It receives a void* context,
|
||||||
|
* which it casts back to the original std::function object to invoke it.
|
||||||
|
*/
|
||||||
|
extern "C" void progress_trampoline(u_int64_t progress, void* context) {
|
||||||
|
if (context) {
|
||||||
|
auto& callback_fn = *static_cast<std::function<void(u_int64_t)>*>(context);
|
||||||
|
callback_fn(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// -------- Factory Methods --------
|
||||||
|
|
||||||
|
Result<InstallationProxy, FfiError> InstallationProxy::connect(Provider& provider) {
|
||||||
|
InstallationProxyClientHandle* handle = nullptr;
|
||||||
|
FfiError e(::installation_proxy_connect(provider.raw(), &handle));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(InstallationProxy::adopt(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<InstallationProxy, FfiError> InstallationProxy::from_socket(Idevice&& socket) {
|
||||||
|
InstallationProxyClientHandle* handle = nullptr;
|
||||||
|
// The Rust FFI function consumes the socket, so we must release it from the
|
||||||
|
// C++ RAII wrapper's control. An `Idevice::release()` method is assumed here.
|
||||||
|
FfiError e(::installation_proxy_new(socket.release(), &handle));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(InstallationProxy::adopt(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Ops --------
|
||||||
|
|
||||||
|
Result<std::vector<plist_t>, FfiError>
|
||||||
|
InstallationProxy::get_apps(Option<std::string> application_type,
|
||||||
|
Option<std::vector<std::string>> bundle_identifiers) {
|
||||||
|
plist_t* apps_raw = nullptr;
|
||||||
|
size_t apps_len = 0;
|
||||||
|
|
||||||
|
const char* application_type_ptr = NULL;
|
||||||
|
if (application_type.is_some()) {
|
||||||
|
application_type_ptr = application_type.unwrap().c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const char*> c_bundle_id;
|
||||||
|
size_t bundle_identifiers_len = 0;
|
||||||
|
if (bundle_identifiers.is_some()) {
|
||||||
|
c_bundle_id.reserve(bundle_identifiers.unwrap().size());
|
||||||
|
for (auto& a : bundle_identifiers.unwrap()) {
|
||||||
|
c_bundle_id.push_back(a.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_get_apps(
|
||||||
|
this->raw(),
|
||||||
|
application_type_ptr,
|
||||||
|
c_bundle_id.empty() ? nullptr : const_cast<const char* const*>(c_bundle_id.data()),
|
||||||
|
bundle_identifiers_len,
|
||||||
|
apps_raw,
|
||||||
|
&apps_len));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<plist_t> apps;
|
||||||
|
if (apps_raw) {
|
||||||
|
apps.assign(apps_raw, apps_raw + apps_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(std::move(apps));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> InstallationProxy::install(std::string package_path,
|
||||||
|
Option<plist_t> options) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_install(this->raw(), package_path.c_str(), &unwrapped_options));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> InstallationProxy::install_with_callback(
|
||||||
|
std::string package_path, Option<plist_t> options, std::function<void(u_int64_t)>& lambda
|
||||||
|
|
||||||
|
) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_install_with_callback(
|
||||||
|
this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> InstallationProxy::upgrade(std::string package_path,
|
||||||
|
Option<plist_t> options) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_upgrade(this->raw(), package_path.c_str(), &unwrapped_options));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> InstallationProxy::upgrade_with_callback(
|
||||||
|
std::string package_path, Option<plist_t> options, std::function<void(u_int64_t)>& lambda
|
||||||
|
|
||||||
|
) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_upgrade_with_callback(
|
||||||
|
this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> InstallationProxy::uninstall(std::string package_path,
|
||||||
|
Option<plist_t> options) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(
|
||||||
|
::installation_proxy_uninstall(this->raw(), package_path.c_str(), &unwrapped_options));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> InstallationProxy::uninstall_with_callback(
|
||||||
|
std::string package_path, Option<plist_t> options, std::function<void(u_int64_t)>& lambda
|
||||||
|
|
||||||
|
) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_uninstall_with_callback(
|
||||||
|
this->raw(), package_path.c_str(), &unwrapped_options, progress_trampoline, &lambda));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<bool, FfiError>
|
||||||
|
InstallationProxy::check_capabilities_match(std::vector<plist_t> capabilities,
|
||||||
|
Option<plist_t> options) {
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool res = false;
|
||||||
|
FfiError e(::installation_proxy_check_capabilities_match(
|
||||||
|
this->raw(),
|
||||||
|
capabilities.empty() ? nullptr : capabilities.data(),
|
||||||
|
capabilities.size(),
|
||||||
|
unwrapped_options,
|
||||||
|
&res));
|
||||||
|
return e ? Result<bool, FfiError>(Err(e)) : Result<bool, FfiError>(Ok(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<plist_t>, FfiError> InstallationProxy::browse(Option<plist_t> options) {
|
||||||
|
plist_t* apps_raw = nullptr;
|
||||||
|
size_t apps_len = 0;
|
||||||
|
|
||||||
|
plist_t unwrapped_options;
|
||||||
|
if (options.is_some()) {
|
||||||
|
unwrapped_options = std::move(options).unwrap();
|
||||||
|
} else {
|
||||||
|
unwrapped_options = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::installation_proxy_browse(this->raw(), unwrapped_options, &apps_raw, &apps_len));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<plist_t> apps;
|
||||||
|
if (apps_raw) {
|
||||||
|
apps.assign(apps_raw, apps_raw + apps_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(std::move(apps));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
247
cpp/src/mobile_image_mounter.cpp
Normal file
247
cpp/src/mobile_image_mounter.cpp
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <idevice++/mobile_image_mounter.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
// -------- Anonymous Namespace for Helpers --------
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
* @brief A C-style trampoline function to call back into a C++ std::function.
|
||||||
|
*
|
||||||
|
* This function is passed to the Rust FFI layer. It receives a void* context,
|
||||||
|
* which it casts back to the original std::function object to invoke it.
|
||||||
|
*/
|
||||||
|
extern "C" void progress_trampoline(size_t progress, size_t total, void* context) {
|
||||||
|
if (context) {
|
||||||
|
auto& callback_fn = *static_cast<std::function<void(size_t, size_t)>*>(context);
|
||||||
|
callback_fn(progress, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// -------- Factory Methods --------
|
||||||
|
|
||||||
|
Result<MobileImageMounter, FfiError> MobileImageMounter::connect(Provider& provider) {
|
||||||
|
ImageMounterHandle* handle = nullptr;
|
||||||
|
FfiError e(::image_mounter_connect(provider.raw(), &handle));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(MobileImageMounter::adopt(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<MobileImageMounter, FfiError> MobileImageMounter::from_socket(Idevice&& socket) {
|
||||||
|
ImageMounterHandle* handle = nullptr;
|
||||||
|
// The Rust FFI function consumes the socket, so we must release it from the
|
||||||
|
// C++ RAII wrapper's control. An `Idevice::release()` method is assumed here.
|
||||||
|
FfiError e(::image_mounter_new(socket.release(), &handle));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(MobileImageMounter::adopt(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Ops --------
|
||||||
|
|
||||||
|
Result<std::vector<plist_t>, FfiError> MobileImageMounter::copy_devices() {
|
||||||
|
plist_t* devices_raw = nullptr;
|
||||||
|
size_t devices_len = 0;
|
||||||
|
|
||||||
|
FfiError e(::image_mounter_copy_devices(this->raw(), &devices_raw, &devices_len));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<plist_t> devices;
|
||||||
|
if (devices_raw) {
|
||||||
|
devices.assign(devices_raw, devices_raw + devices_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(std::move(devices));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<uint8_t>, FfiError> MobileImageMounter::lookup_image(std::string image_type) {
|
||||||
|
uint8_t* signature_raw = nullptr;
|
||||||
|
size_t signature_len = 0;
|
||||||
|
|
||||||
|
FfiError e(::image_mounter_lookup_image(
|
||||||
|
this->raw(), image_type.c_str(), &signature_raw, &signature_len));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> signature(signature_len);
|
||||||
|
std::memcpy(signature.data(), signature_raw, signature_len);
|
||||||
|
idevice_data_free(signature_raw, signature_len);
|
||||||
|
|
||||||
|
return Ok(std::move(signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::upload_image(std::string image_type,
|
||||||
|
const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* signature_data,
|
||||||
|
size_t signature_size) {
|
||||||
|
FfiError e(::image_mounter_upload_image(
|
||||||
|
this->raw(), image_type.c_str(), image_data, image_size, signature_data, signature_size));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::mount_image(std::string image_type,
|
||||||
|
const uint8_t* signature_data,
|
||||||
|
size_t signature_size,
|
||||||
|
const uint8_t* trust_cache_data,
|
||||||
|
size_t trust_cache_size,
|
||||||
|
plist_t info_plist) {
|
||||||
|
FfiError e(::image_mounter_mount_image(this->raw(),
|
||||||
|
image_type.c_str(),
|
||||||
|
signature_data,
|
||||||
|
signature_size,
|
||||||
|
trust_cache_data,
|
||||||
|
trust_cache_size,
|
||||||
|
info_plist));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::unmount_image(std::string mount_path) {
|
||||||
|
FfiError e(::image_mounter_unmount_image(this->raw(), mount_path.c_str()));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<bool, FfiError> MobileImageMounter::query_developer_mode_status() {
|
||||||
|
int status_c = 0;
|
||||||
|
FfiError e(::image_mounter_query_developer_mode_status(this->raw(), &status_c));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(status_c != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::mount_developer(const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* signature_data,
|
||||||
|
size_t signature_size) {
|
||||||
|
FfiError e(::image_mounter_mount_developer(
|
||||||
|
this->raw(), image_data, image_size, signature_data, signature_size));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<uint8_t>, FfiError> MobileImageMounter::query_personalization_manifest(
|
||||||
|
std::string image_type, const uint8_t* signature_data, size_t signature_size) {
|
||||||
|
uint8_t* manifest_raw = nullptr;
|
||||||
|
size_t manifest_len = 0;
|
||||||
|
FfiError e(::image_mounter_query_personalization_manifest(this->raw(),
|
||||||
|
image_type.c_str(),
|
||||||
|
signature_data,
|
||||||
|
signature_size,
|
||||||
|
&manifest_raw,
|
||||||
|
&manifest_len));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> manifest(manifest_len);
|
||||||
|
std::memcpy(manifest.data(), manifest_raw, manifest_len);
|
||||||
|
idevice_data_free(manifest_raw, manifest_len);
|
||||||
|
|
||||||
|
return Ok(std::move(manifest));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<uint8_t>, FfiError>
|
||||||
|
MobileImageMounter::query_nonce(std::string personalized_image_type) {
|
||||||
|
uint8_t* nonce_raw = nullptr;
|
||||||
|
size_t nonce_len = 0;
|
||||||
|
const char* image_type_c =
|
||||||
|
personalized_image_type.empty() ? nullptr : personalized_image_type.c_str();
|
||||||
|
|
||||||
|
FfiError e(::image_mounter_query_nonce(this->raw(), image_type_c, &nonce_raw, &nonce_len));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> nonce(nonce_len);
|
||||||
|
std::memcpy(nonce.data(), nonce_raw, nonce_len);
|
||||||
|
idevice_data_free(nonce_raw, nonce_len);
|
||||||
|
|
||||||
|
return Ok(std::move(nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<plist_t, FfiError>
|
||||||
|
MobileImageMounter::query_personalization_identifiers(std::string image_type) {
|
||||||
|
plist_t identifiers = nullptr;
|
||||||
|
const char* image_type_c = image_type.empty() ? nullptr : image_type.c_str();
|
||||||
|
|
||||||
|
FfiError e(
|
||||||
|
::image_mounter_query_personalization_identifiers(this->raw(), image_type_c, &identifiers));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The caller now owns the returned `plist_t` and is responsible for freeing it.
|
||||||
|
return Ok(identifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::roll_personalization_nonce() {
|
||||||
|
FfiError e(::image_mounter_roll_personalization_nonce(this->raw()));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::roll_cryptex_nonce() {
|
||||||
|
FfiError e(::image_mounter_roll_cryptex_nonce(this->raw()));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> MobileImageMounter::mount_personalized(Provider& provider,
|
||||||
|
const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* trust_cache_data,
|
||||||
|
size_t trust_cache_size,
|
||||||
|
const uint8_t* build_manifest_data,
|
||||||
|
size_t build_manifest_size,
|
||||||
|
plist_t info_plist,
|
||||||
|
uint64_t unique_chip_id) {
|
||||||
|
FfiError e(::image_mounter_mount_personalized(this->raw(),
|
||||||
|
provider.raw(),
|
||||||
|
image_data,
|
||||||
|
image_size,
|
||||||
|
trust_cache_data,
|
||||||
|
trust_cache_size,
|
||||||
|
build_manifest_data,
|
||||||
|
build_manifest_size,
|
||||||
|
info_plist,
|
||||||
|
unique_chip_id));
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError>
|
||||||
|
MobileImageMounter::mount_personalized_with_callback(Provider& provider,
|
||||||
|
const uint8_t* image_data,
|
||||||
|
size_t image_size,
|
||||||
|
const uint8_t* trust_cache_data,
|
||||||
|
size_t trust_cache_size,
|
||||||
|
const uint8_t* build_manifest_data,
|
||||||
|
size_t build_manifest_size,
|
||||||
|
plist_t info_plist,
|
||||||
|
uint64_t unique_chip_id,
|
||||||
|
std::function<void(size_t, size_t)>& lambda) {
|
||||||
|
|
||||||
|
FfiError e(::image_mounter_mount_personalized_with_callback(this->raw(),
|
||||||
|
provider.raw(),
|
||||||
|
image_data,
|
||||||
|
image_size,
|
||||||
|
trust_cache_data,
|
||||||
|
trust_cache_size,
|
||||||
|
build_manifest_data,
|
||||||
|
build_manifest_size,
|
||||||
|
info_plist,
|
||||||
|
unique_chip_id,
|
||||||
|
progress_trampoline,
|
||||||
|
&lambda /* context */));
|
||||||
|
|
||||||
|
return e ? Result<void, FfiError>(Err(e)) : Result<void, FfiError>(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
73
cpp/src/process_control.cpp
Normal file
73
cpp/src/process_control.cpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <idevice++/process_control.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
Result<ProcessControl, FfiError> ProcessControl::create(RemoteServer& server) {
|
||||||
|
ProcessControlHandle* out = nullptr;
|
||||||
|
FfiError e(::process_control_new(server.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(ProcessControl::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<u_int64_t, FfiError> ProcessControl::launch_app(std::string bundle_id,
|
||||||
|
Option<std::vector<std::string>> env_vars,
|
||||||
|
Option<std::vector<std::string>> arguments,
|
||||||
|
bool start_suspended,
|
||||||
|
bool kill_existing) {
|
||||||
|
std::vector<const char*> c_env_vars;
|
||||||
|
size_t env_vars_len = 0;
|
||||||
|
if (env_vars.is_some()) {
|
||||||
|
c_env_vars.reserve(env_vars.unwrap().size());
|
||||||
|
for (auto& a : env_vars.unwrap()) {
|
||||||
|
c_env_vars.push_back(a.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const char*> c_arguments;
|
||||||
|
size_t arguments_len = 0;
|
||||||
|
if (arguments.is_some()) {
|
||||||
|
c_arguments.reserve(arguments.unwrap().size());
|
||||||
|
for (auto& a : arguments.unwrap()) {
|
||||||
|
c_arguments.push_back(a.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u_int64_t pid = 0;
|
||||||
|
|
||||||
|
FfiError e(::process_control_launch_app(
|
||||||
|
handle_.get(),
|
||||||
|
bundle_id.c_str(),
|
||||||
|
c_env_vars.empty() ? nullptr : const_cast<const char* const*>(c_env_vars.data()),
|
||||||
|
env_vars_len,
|
||||||
|
c_arguments.empty() ? nullptr : const_cast<const char* const*>(c_arguments.data()),
|
||||||
|
arguments_len,
|
||||||
|
start_suspended,
|
||||||
|
kill_existing,
|
||||||
|
&pid));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> ProcessControl::kill_app(u_int64_t pid) {
|
||||||
|
FfiError e(::process_control_kill_app(handle_.get(), pid));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> ProcessControl::disable_memory_limit(u_int64_t pid) {
|
||||||
|
FfiError e(::process_control_disable_memory_limit(handle_.get(), pid));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
@@ -13,7 +13,7 @@ once_cell = "1.21.1"
|
|||||||
tokio = { version = "1.44.1", features = ["full"] }
|
tokio = { version = "1.44.1", features = ["full"] }
|
||||||
libc = "0.2.171"
|
libc = "0.2.171"
|
||||||
plist = "1.7.1"
|
plist = "1.7.1"
|
||||||
plist_ffi = { version = "0.1.5" }
|
plist_ffi = { version = "0.1.6" }
|
||||||
uuid = { version = "1.12", features = ["v4"], optional = true }
|
uuid = { version = "1.12", features = ["v4"], optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub struct InstallationProxyClientHandle(pub InstallationProxyClient);
|
|||||||
/// `provider` must be a valid pointer to a handle allocated by this library
|
/// `provider` must be a valid pointer to a handle allocated by this library
|
||||||
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn installation_proxy_connect_tcp(
|
pub unsafe extern "C" fn installation_proxy_connect(
|
||||||
provider: *mut IdeviceProviderHandle,
|
provider: *mut IdeviceProviderHandle,
|
||||||
client: *mut *mut InstallationProxyClientHandle,
|
client: *mut *mut InstallationProxyClientHandle,
|
||||||
) -> *mut IdeviceFfiError {
|
) -> *mut IdeviceFfiError {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "idevice"
|
name = "idevice"
|
||||||
description = "A Rust library to interact with services on iOS devices."
|
description = "A Rust library to interact with services on iOS devices."
|
||||||
authors = ["Jackson Coxson"]
|
authors = ["Jackson Coxson"]
|
||||||
version = "0.1.41"
|
version = "0.1.42"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
documentation = "https://docs.rs/idevice"
|
documentation = "https://docs.rs/idevice"
|
||||||
@@ -60,6 +60,8 @@ chacha20poly1305 = { version = "0.10", optional = true }
|
|||||||
|
|
||||||
obfstr = { version = "0.4", optional = true }
|
obfstr = { version = "0.4", optional = true }
|
||||||
|
|
||||||
|
async_zip = { version = "0.0.18", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
tun-rs = { version = "2.0.8", features = ["async_tokio"] }
|
tun-rs = { version = "2.0.8", features = ["async_tokio"] }
|
||||||
@@ -82,10 +84,17 @@ diagnostics_relay = []
|
|||||||
dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
|
dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
|
||||||
heartbeat = ["tokio/macros", "tokio/time"]
|
heartbeat = ["tokio/macros", "tokio/time"]
|
||||||
house_arrest = ["afc"]
|
house_arrest = ["afc"]
|
||||||
installation_proxy = []
|
installation_proxy = [
|
||||||
|
"dep:async_zip",
|
||||||
|
"dep:futures",
|
||||||
|
"async_zip/tokio",
|
||||||
|
"async_zip/deflate",
|
||||||
|
"tokio/fs",
|
||||||
|
]
|
||||||
springboardservices = []
|
springboardservices = []
|
||||||
misagent = []
|
misagent = []
|
||||||
mobile_image_mounter = ["dep:sha2"]
|
mobile_image_mounter = ["dep:sha2"]
|
||||||
|
mobileactivationd = ["dep:reqwest"]
|
||||||
mobilebackup2 = []
|
mobilebackup2 = []
|
||||||
location_simulation = []
|
location_simulation = []
|
||||||
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
|
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
|
||||||
@@ -102,6 +111,7 @@ remote_pairing = [
|
|||||||
"dep:uuid",
|
"dep:uuid",
|
||||||
]
|
]
|
||||||
rsd = ["xpc"]
|
rsd = ["xpc"]
|
||||||
|
screenshotr = []
|
||||||
syslog_relay = ["dep:bytes"]
|
syslog_relay = ["dep:bytes"]
|
||||||
tcp = ["tokio/net"]
|
tcp = ["tokio/net"]
|
||||||
tunnel_tcp_stack = [
|
tunnel_tcp_stack = [
|
||||||
@@ -132,6 +142,7 @@ full = [
|
|||||||
"location_simulation",
|
"location_simulation",
|
||||||
"misagent",
|
"misagent",
|
||||||
"mobile_image_mounter",
|
"mobile_image_mounter",
|
||||||
|
"mobileactivationd",
|
||||||
"mobilebackup2",
|
"mobilebackup2",
|
||||||
"pair",
|
"pair",
|
||||||
"pcapd",
|
"pcapd",
|
||||||
@@ -142,6 +153,7 @@ full = [
|
|||||||
"location_simulation",
|
"location_simulation",
|
||||||
"remote_pairing",
|
"remote_pairing",
|
||||||
"rsd",
|
"rsd",
|
||||||
|
"screenshotr",
|
||||||
"springboardservices",
|
"springboardservices",
|
||||||
"syslog_relay",
|
"syslog_relay",
|
||||||
"tcp",
|
"tcp",
|
||||||
|
|||||||
@@ -692,32 +692,16 @@ pub enum IdeviceError {
|
|||||||
#[error("failed to parse bytes as valid utf8")]
|
#[error("failed to parse bytes as valid utf8")]
|
||||||
Utf8Error = -56,
|
Utf8Error = -56,
|
||||||
|
|
||||||
#[cfg(feature = "debug_proxy")]
|
#[cfg(any(
|
||||||
|
feature = "debug_proxy",
|
||||||
|
all(feature = "afc", feature = "installation_proxy")
|
||||||
|
))]
|
||||||
#[error("invalid argument passed")]
|
#[error("invalid argument passed")]
|
||||||
InvalidArgument = -57,
|
InvalidArgument = -57,
|
||||||
|
|
||||||
#[error("unknown error `{0}` returned from device")]
|
#[error("unknown error `{0}` returned from device")]
|
||||||
UnknownErrorType(String) = -59,
|
UnknownErrorType(String) = -59,
|
||||||
|
|
||||||
#[cfg(feature = "remote_pairing")]
|
|
||||||
#[error("could not parse as JSON")]
|
|
||||||
JsonParseFailed(#[from] json::Error) = -67,
|
|
||||||
|
|
||||||
#[cfg(feature = "remote_pairing")]
|
|
||||||
#[error("unknown TLV type: {0}")]
|
|
||||||
UnknownTlv(u8) = -68,
|
|
||||||
|
|
||||||
#[cfg(feature = "remote_pairing")]
|
|
||||||
#[error("malformed TLV")]
|
|
||||||
MalformedTlv = -69,
|
|
||||||
|
|
||||||
#[error("failed to decode base64 string")]
|
|
||||||
Base64Decode(#[from] base64::DecodeError) = -70,
|
|
||||||
|
|
||||||
#[cfg(feature = "remote_pairing")]
|
|
||||||
#[error("pair verify failed")]
|
|
||||||
PairVerifyFailed = -71,
|
|
||||||
|
|
||||||
#[error("invalid arguments were passed")]
|
#[error("invalid arguments were passed")]
|
||||||
FfiInvalidArg = -60,
|
FfiInvalidArg = -60,
|
||||||
#[error("invalid string was passed")]
|
#[error("invalid string was passed")]
|
||||||
@@ -732,6 +716,32 @@ pub enum IdeviceError {
|
|||||||
IntegerOverflow = -65,
|
IntegerOverflow = -65,
|
||||||
#[error("canceled by user")]
|
#[error("canceled by user")]
|
||||||
CanceledByUser = -66,
|
CanceledByUser = -66,
|
||||||
|
|
||||||
|
#[cfg(feature = "installation_proxy")]
|
||||||
|
#[error("malformed package archive: {0}")]
|
||||||
|
MalformedPackageArchive(#[from] async_zip::error::ZipError) = -67,
|
||||||
|
|
||||||
|
#[error("Developer mode is not enabled")]
|
||||||
|
DeveloperModeNotEnabled = -68,
|
||||||
|
|
||||||
|
#[cfg(feature = "remote_pairing")]
|
||||||
|
#[error("could not parse as JSON")]
|
||||||
|
JsonParseFailed(#[from] json::Error) = -69,
|
||||||
|
|
||||||
|
#[cfg(feature = "remote_pairing")]
|
||||||
|
#[error("unknown TLV type: {0}")]
|
||||||
|
UnknownTlv(u8) = -70,
|
||||||
|
|
||||||
|
#[cfg(feature = "remote_pairing")]
|
||||||
|
#[error("malformed TLV")]
|
||||||
|
MalformedTlv = -71,
|
||||||
|
|
||||||
|
#[error("failed to decode base64 string")]
|
||||||
|
Base64Decode(#[from] base64::DecodeError) = -72,
|
||||||
|
|
||||||
|
#[cfg(feature = "remote_pairing")]
|
||||||
|
#[error("pair verify failed")]
|
||||||
|
PairVerifyFailed = -73,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdeviceError {
|
impl IdeviceError {
|
||||||
@@ -746,6 +756,8 @@ impl IdeviceError {
|
|||||||
fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option<Self> {
|
fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option<Self> {
|
||||||
if e.contains("NSDebugDescription=Canceled by user.") {
|
if e.contains("NSDebugDescription=Canceled by user.") {
|
||||||
return Some(Self::CanceledByUser);
|
return Some(Self::CanceledByUser);
|
||||||
|
} else if e.contains("Developer mode is not enabled.") {
|
||||||
|
return Some(Self::DeveloperModeNotEnabled);
|
||||||
}
|
}
|
||||||
match e {
|
match e {
|
||||||
"GetProhibited" => Some(Self::GetProhibited),
|
"GetProhibited" => Some(Self::GetProhibited),
|
||||||
@@ -876,7 +888,10 @@ impl IdeviceError {
|
|||||||
IdeviceError::NotEnoughBytes(_, _) => -55,
|
IdeviceError::NotEnoughBytes(_, _) => -55,
|
||||||
IdeviceError::Utf8Error => -56,
|
IdeviceError::Utf8Error => -56,
|
||||||
|
|
||||||
#[cfg(feature = "debug_proxy")]
|
#[cfg(any(
|
||||||
|
feature = "debug_proxy",
|
||||||
|
all(feature = "afc", feature = "installation_proxy")
|
||||||
|
))]
|
||||||
IdeviceError::InvalidArgument => -57,
|
IdeviceError::InvalidArgument => -57,
|
||||||
|
|
||||||
IdeviceError::UnknownErrorType(_) => -59,
|
IdeviceError::UnknownErrorType(_) => -59,
|
||||||
@@ -888,15 +903,18 @@ impl IdeviceError {
|
|||||||
IdeviceError::IntegerOverflow => -65,
|
IdeviceError::IntegerOverflow => -65,
|
||||||
IdeviceError::CanceledByUser => -66,
|
IdeviceError::CanceledByUser => -66,
|
||||||
|
|
||||||
|
#[cfg(feature = "installation_proxy")]
|
||||||
|
IdeviceError::MalformedPackageArchive(_) => -67,
|
||||||
|
IdeviceError::DeveloperModeNotEnabled => -68,
|
||||||
#[cfg(feature = "remote_pairing")]
|
#[cfg(feature = "remote_pairing")]
|
||||||
IdeviceError::JsonParseFailed(_) => -67,
|
IdeviceError::JsonParseFailed(_) => -69,
|
||||||
#[cfg(feature = "remote_pairing")]
|
#[cfg(feature = "remote_pairing")]
|
||||||
IdeviceError::UnknownTlv(_) => -68,
|
IdeviceError::UnknownTlv(_) => -70,
|
||||||
#[cfg(feature = "remote_pairing")]
|
#[cfg(feature = "remote_pairing")]
|
||||||
IdeviceError::MalformedTlv => -69,
|
IdeviceError::MalformedTlv => -71,
|
||||||
IdeviceError::Base64Decode(_) => -70,
|
IdeviceError::Base64Decode(_) => -72,
|
||||||
#[cfg(feature = "remote_pairing")]
|
#[cfg(feature = "remote_pairing")]
|
||||||
IdeviceError::PairVerifyFailed => -71,
|
IdeviceError::PairVerifyFailed => -73,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
|
||||||
use crate::IdeviceError;
|
use crate::IdeviceError;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -19,28 +21,76 @@ pub struct FileDescriptor<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileDescriptor<'_> {
|
impl FileDescriptor<'_> {
|
||||||
/// Closes the file descriptor
|
/// Generic helper to send an AFC packet and read the response
|
||||||
pub async fn close(self) -> Result<(), IdeviceError> {
|
async fn send_packet(
|
||||||
let header_payload = self.fd.to_le_bytes().to_vec();
|
&mut self,
|
||||||
|
opcode: AfcOpcode,
|
||||||
|
header_payload: Vec<u8>,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
) -> Result<AfcPacket, IdeviceError> {
|
||||||
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
|
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
|
||||||
|
|
||||||
let header = AfcPacketHeader {
|
let header = AfcPacketHeader {
|
||||||
magic: super::MAGIC,
|
magic: super::MAGIC,
|
||||||
entire_len: header_len,
|
entire_len: header_len + payload.len() as u64,
|
||||||
header_payload_len: header_len,
|
header_payload_len: header_len,
|
||||||
packet_num: self.client.package_number,
|
packet_num: self.client.package_number,
|
||||||
operation: AfcOpcode::FileClose,
|
operation: opcode,
|
||||||
};
|
};
|
||||||
self.client.package_number += 1;
|
self.client.package_number += 1;
|
||||||
|
|
||||||
let packet = AfcPacket {
|
let packet = AfcPacket {
|
||||||
header,
|
header,
|
||||||
header_payload,
|
header_payload,
|
||||||
payload: Vec::new(),
|
payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.client.send(packet).await?;
|
self.client.send(packet).await?;
|
||||||
self.client.read().await?;
|
self.client.read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current cursor position for the file
|
||||||
|
pub async fn seek_tell(&mut self) -> Result<u64, IdeviceError> {
|
||||||
|
let header_payload = self.fd.to_le_bytes().to_vec();
|
||||||
|
let res = self
|
||||||
|
.send_packet(AfcOpcode::FileTell, header_payload, Vec::new())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cur_pos = res
|
||||||
|
.header_payload
|
||||||
|
.get(..8)
|
||||||
|
.ok_or(IdeviceError::UnexpectedResponse)?
|
||||||
|
.try_into()
|
||||||
|
.map(u64::from_le_bytes)
|
||||||
|
.map_err(|_| IdeviceError::UnexpectedResponse)?;
|
||||||
|
|
||||||
|
Ok(cur_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the file cursor
|
||||||
|
pub async fn seek(&mut self, pos: SeekFrom) -> Result<(), IdeviceError> {
|
||||||
|
let (offset, whence) = match pos {
|
||||||
|
SeekFrom::Start(off) => (off as i64, 0),
|
||||||
|
SeekFrom::Current(off) => (off, 1),
|
||||||
|
SeekFrom::End(off) => (off, 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut header_payload = Vec::new();
|
||||||
|
header_payload.extend(self.fd.to_le_bytes());
|
||||||
|
header_payload.extend((whence as u64).to_le_bytes());
|
||||||
|
header_payload.extend(offset.to_le_bytes());
|
||||||
|
|
||||||
|
self.send_packet(AfcOpcode::FileSeek, header_payload, Vec::new())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the file descriptor
|
||||||
|
pub async fn close(mut self) -> Result<(), IdeviceError> {
|
||||||
|
let header_payload = self.fd.to_le_bytes().to_vec();
|
||||||
|
|
||||||
|
self.send_packet(AfcOpcode::FileClose, header_payload, Vec::new())
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,32 +99,18 @@ impl FileDescriptor<'_> {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// A vector containing the file's data
|
/// A vector containing the file's data
|
||||||
pub async fn read(&mut self) -> Result<Vec<u8>, IdeviceError> {
|
pub async fn read(&mut self) -> Result<Vec<u8>, IdeviceError> {
|
||||||
// Get the file size first
|
let seek_pos = self.seek_tell().await? as usize;
|
||||||
let mut bytes_left = self.client.get_file_info(&self.path).await?.size;
|
let file_info = self.client.get_file_info(&self.path).await?;
|
||||||
|
let mut bytes_left = file_info.size.saturating_sub(seek_pos);
|
||||||
let mut collected_bytes = Vec::with_capacity(bytes_left);
|
let mut collected_bytes = Vec::with_capacity(bytes_left);
|
||||||
|
|
||||||
while bytes_left > 0 {
|
while bytes_left > 0 {
|
||||||
let mut header_payload = self.fd.to_le_bytes().to_vec();
|
let mut header_payload = self.fd.to_le_bytes().to_vec();
|
||||||
header_payload.extend_from_slice(&MAX_TRANSFER.to_le_bytes());
|
header_payload.extend_from_slice(&MAX_TRANSFER.to_le_bytes());
|
||||||
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
|
let res = self
|
||||||
|
.send_packet(AfcOpcode::Read, header_payload, Vec::new())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let header = AfcPacketHeader {
|
|
||||||
magic: super::MAGIC,
|
|
||||||
entire_len: header_len,
|
|
||||||
header_payload_len: header_len,
|
|
||||||
packet_num: self.client.package_number,
|
|
||||||
operation: AfcOpcode::Read,
|
|
||||||
};
|
|
||||||
self.client.package_number += 1;
|
|
||||||
|
|
||||||
let packet = AfcPacket {
|
|
||||||
header,
|
|
||||||
header_payload,
|
|
||||||
payload: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.client.send(packet).await?;
|
|
||||||
let res = self.client.read().await?;
|
|
||||||
bytes_left -= res.payload.len();
|
bytes_left -= res.payload.len();
|
||||||
collected_bytes.extend(res.payload);
|
collected_bytes.extend(res.payload);
|
||||||
}
|
}
|
||||||
@@ -87,29 +123,10 @@ impl FileDescriptor<'_> {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `bytes` - Data to write to the file
|
/// * `bytes` - Data to write to the file
|
||||||
pub async fn write(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> {
|
pub async fn write(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> {
|
||||||
let chunks = bytes.chunks(MAX_TRANSFER as usize);
|
for chunk in bytes.chunks(MAX_TRANSFER as usize) {
|
||||||
|
|
||||||
for chunk in chunks {
|
|
||||||
let header_payload = self.fd.to_le_bytes().to_vec();
|
let header_payload = self.fd.to_le_bytes().to_vec();
|
||||||
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
|
self.send_packet(AfcOpcode::Write, header_payload, chunk.to_vec())
|
||||||
|
.await?;
|
||||||
let header = AfcPacketHeader {
|
|
||||||
magic: super::MAGIC,
|
|
||||||
entire_len: header_len + chunk.len() as u64,
|
|
||||||
header_payload_len: header_len,
|
|
||||||
packet_num: self.client.package_number,
|
|
||||||
operation: AfcOpcode::Write,
|
|
||||||
};
|
|
||||||
self.client.package_number += 1;
|
|
||||||
|
|
||||||
let packet = AfcPacket {
|
|
||||||
header,
|
|
||||||
header_payload,
|
|
||||||
payload: chunk.to_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.client.send(packet).await?;
|
|
||||||
self.client.read().await?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,24 +392,37 @@ impl Message {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// * Various IdeviceError variants for IO and parsing failures
|
/// * Various IdeviceError variants for IO and parsing failures
|
||||||
pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Self, IdeviceError> {
|
pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Self, IdeviceError> {
|
||||||
let mut buf = [0u8; 32];
|
let mut packet_data: Vec<u8> = Vec::new();
|
||||||
reader.read_exact(&mut buf).await?;
|
// loop for deal with multiple fragments
|
||||||
|
let mheader = loop {
|
||||||
let mheader = MessageHeader {
|
let mut buf = [0u8; 32];
|
||||||
magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
|
reader.read_exact(&mut buf).await?;
|
||||||
header_len: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
let header = MessageHeader {
|
||||||
fragment_id: u16::from_le_bytes([buf[8], buf[9]]),
|
magic: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
|
||||||
fragment_count: u16::from_le_bytes([buf[10], buf[11]]),
|
header_len: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
||||||
length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
|
fragment_id: u16::from_le_bytes([buf[8], buf[9]]),
|
||||||
identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
|
fragment_count: u16::from_le_bytes([buf[10], buf[11]]),
|
||||||
conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
|
length: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
|
||||||
channel: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]),
|
identifier: u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
|
||||||
expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1,
|
conversation_index: u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]),
|
||||||
|
//treat both as the negative and positive representation of the channel code in the response
|
||||||
|
// the same when performing fragmentation
|
||||||
|
channel: i32::abs(i32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]])) as u32,
|
||||||
|
expects_reply: u32::from_le_bytes([buf[28], buf[29], buf[30], buf[31]]) == 1,
|
||||||
|
};
|
||||||
|
if header.fragment_count > 1 && header.fragment_id == 0 {
|
||||||
|
// when reading multiple message fragments, the first fragment contains only a message header.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut buf = vec![0u8; header.length as usize];
|
||||||
|
reader.read_exact(&mut buf).await?;
|
||||||
|
packet_data.extend(buf);
|
||||||
|
if header.fragment_id == header.fragment_count - 1 {
|
||||||
|
break header;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
// read the payload header
|
||||||
let mut buf = [0u8; 16];
|
let buf = &packet_data[0..16];
|
||||||
reader.read_exact(&mut buf).await?;
|
|
||||||
|
|
||||||
let pheader = PayloadHeader {
|
let pheader = PayloadHeader {
|
||||||
flags: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
|
flags: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
|
||||||
aux_length: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
aux_length: u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
||||||
@@ -417,18 +430,17 @@ impl Message {
|
|||||||
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
|
buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15],
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let aux = if pheader.aux_length > 0 {
|
let aux = if pheader.aux_length > 0 {
|
||||||
let mut buf = vec![0u8; pheader.aux_length as usize];
|
let buf = packet_data[16..(16 + pheader.aux_length as usize)].to_vec();
|
||||||
reader.read_exact(&mut buf).await?;
|
|
||||||
Some(Aux::from_bytes(buf)?)
|
Some(Aux::from_bytes(buf)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
// read the data
|
||||||
let mut buf = vec![0u8; (pheader.total_length - pheader.aux_length as u64) as usize];
|
let need_len = (pheader.total_length - pheader.aux_length as u64) as usize;
|
||||||
reader.read_exact(&mut buf).await?;
|
let buf = packet_data
|
||||||
|
[(pheader.aux_length + 16) as usize..pheader.aux_length as usize + 16 + need_len]
|
||||||
|
.to_vec();
|
||||||
let data = if buf.is_empty() {
|
let data = if buf.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ use crate::{Idevice, IdeviceError, ReadWrite, RsdService, obf};
|
|||||||
#[cfg(feature = "location_simulation")]
|
#[cfg(feature = "location_simulation")]
|
||||||
pub mod location_simulation;
|
pub mod location_simulation;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod notifications;
|
||||||
pub mod process_control;
|
pub mod process_control;
|
||||||
pub mod remote_server;
|
pub mod remote_server;
|
||||||
|
pub mod screenshot;
|
||||||
|
|
||||||
impl RsdService for remote_server::RemoteServerClient<Box<dyn ReadWrite>> {
|
impl RsdService for remote_server::RemoteServerClient<Box<dyn ReadWrite>> {
|
||||||
fn rsd_service_name() -> std::borrow::Cow<'static, str> {
|
fn rsd_service_name() -> std::borrow::Cow<'static, str> {
|
||||||
|
|||||||
163
idevice/src/services/dvt/notifications.rs
Normal file
163
idevice/src/services/dvt/notifications.rs
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
//! Notificaitons service client for iOS instruments protocol.
|
||||||
|
//!
|
||||||
|
//! Monitor memory and app notifications
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
IdeviceError, ReadWrite,
|
||||||
|
dvt::{
|
||||||
|
message::AuxValue,
|
||||||
|
remote_server::{Channel, RemoteServerClient},
|
||||||
|
},
|
||||||
|
obf,
|
||||||
|
};
|
||||||
|
use log::warn;
|
||||||
|
use plist::Value;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NotificationInfo {
|
||||||
|
notification_type: String,
|
||||||
|
mach_absolute_time: i64,
|
||||||
|
exec_name: String,
|
||||||
|
app_name: String,
|
||||||
|
pid: u32,
|
||||||
|
state_description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NotificationsClient<'a, R: ReadWrite> {
|
||||||
|
/// The underlying channel used for communication
|
||||||
|
pub channel: Channel<'a, R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R: ReadWrite> NotificationsClient<'a, R> {
|
||||||
|
/// Opens a new channel on the remote server client for app notifications
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - The remote server client to connect with
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The client on success, IdeviceError on failure
|
||||||
|
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
|
||||||
|
let channel = client
|
||||||
|
.make_channel(obf!(
|
||||||
|
"com.apple.instruments.server.services.mobilenotifications"
|
||||||
|
))
|
||||||
|
.await?; // Drop `&mut client` before continuing
|
||||||
|
|
||||||
|
Ok(Self { channel })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the applicaitons and memory notifications enabled
|
||||||
|
pub async fn start_notifications(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
let application_method = Value::String("setApplicationStateNotificationsEnabled:".into());
|
||||||
|
self.channel
|
||||||
|
.call_method(
|
||||||
|
Some(application_method),
|
||||||
|
Some(vec![AuxValue::archived_value(true)]),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let memory_method = Value::String("setMemoryNotificationsEnabled:".into());
|
||||||
|
self.channel
|
||||||
|
.call_method(
|
||||||
|
Some(memory_method),
|
||||||
|
Some(vec![AuxValue::archived_value(true)]),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the next notification from the service
|
||||||
|
pub async fn get_notification(&mut self) -> Result<NotificationInfo, IdeviceError> {
|
||||||
|
let message = self.channel.read_message().await?;
|
||||||
|
let mut notification = NotificationInfo {
|
||||||
|
notification_type: "".to_string(),
|
||||||
|
mach_absolute_time: 0,
|
||||||
|
exec_name: String::new(),
|
||||||
|
app_name: String::new(),
|
||||||
|
pid: 0,
|
||||||
|
state_description: String::new(),
|
||||||
|
};
|
||||||
|
if let Some(aux) = message.aux {
|
||||||
|
for v in aux.values {
|
||||||
|
match v {
|
||||||
|
AuxValue::Array(a) => match ns_keyed_archive::decode::from_bytes(&a) {
|
||||||
|
Ok(archive) => {
|
||||||
|
if let Some(dict) = archive.into_dictionary() {
|
||||||
|
for (key, value) in dict.into_iter() {
|
||||||
|
match key.as_str() {
|
||||||
|
"mach_absolute_time" => {
|
||||||
|
if let Value::Integer(time) = value {
|
||||||
|
notification.mach_absolute_time =
|
||||||
|
time.as_signed().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"execName" => {
|
||||||
|
if let Value::String(name) = value {
|
||||||
|
notification.exec_name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"appName" => {
|
||||||
|
if let Value::String(name) = value {
|
||||||
|
notification.app_name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"pid" => {
|
||||||
|
if let Value::Integer(pid) = value {
|
||||||
|
notification.pid =
|
||||||
|
pid.as_unsigned().unwrap_or(0) as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"state_description" => {
|
||||||
|
if let Value::String(desc) = value {
|
||||||
|
notification.state_description = desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Unknown notificaton key: {} = {:?}", key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to decode archive: {:?}", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
warn!("Non-array aux value: {:?}", v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::String(data)) = message.data {
|
||||||
|
notification.notification_type = data;
|
||||||
|
Ok(notification)
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the applicaitons and memory notifications disable
|
||||||
|
pub async fn stop_notifications(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
let application_method = Value::String("setApplicationStateNotificationsEnabled:".into());
|
||||||
|
self.channel
|
||||||
|
.call_method(
|
||||||
|
Some(application_method),
|
||||||
|
Some(vec![AuxValue::archived_value(false)]),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let memory_method = Value::String("setMemoryNotificationsEnabled:".into());
|
||||||
|
self.channel
|
||||||
|
.call_method(
|
||||||
|
Some(memory_method),
|
||||||
|
Some(vec![AuxValue::archived_value(false)]),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,6 +201,7 @@ impl<R: ReadWrite> RemoteServerClient<R> {
|
|||||||
|
|
||||||
let message = Message::new(mheader, pheader, aux, data);
|
let message = Message::new(mheader, pheader, aux, data);
|
||||||
debug!("Sending message: {message:#?}");
|
debug!("Sending message: {message:#?}");
|
||||||
|
|
||||||
self.idevice.write_all(&message.serialize()).await?;
|
self.idevice.write_all(&message.serialize()).await?;
|
||||||
self.idevice.flush().await?;
|
self.idevice.flush().await?;
|
||||||
|
|
||||||
|
|||||||
64
idevice/src/services/dvt/screenshot.rs
Normal file
64
idevice/src/services/dvt/screenshot.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
//! Screenshot service client for iOS instruments protocol.
|
||||||
|
//!
|
||||||
|
//! This module provides a client for interacting with the screenshot service
|
||||||
|
//! on iOS devices through the instruments protocol. It allows taking screenshots from the device.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use plist::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
IdeviceError, ReadWrite,
|
||||||
|
dvt::remote_server::{Channel, RemoteServerClient},
|
||||||
|
obf,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Client for take screenshot operations on iOS devices
|
||||||
|
///
|
||||||
|
/// Provides methods for take screnn_shot through the
|
||||||
|
/// instruments protocol. Each instance maintains its own communication channel.
|
||||||
|
pub struct ScreenshotClient<'a, R: ReadWrite> {
|
||||||
|
/// The underlying channel for communication
|
||||||
|
channel: Channel<'a, R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R: ReadWrite> ScreenshotClient<'a, R> {
|
||||||
|
/// Creates a new ScreenshotClient
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - The base RemoteServerClient to use
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(ScreenshotClient)` - Connected client instance
|
||||||
|
/// * `Err(IdeviceError)` - If channel creation fails
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// * Propagates errors from channel creation
|
||||||
|
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
|
||||||
|
let channel = client
|
||||||
|
.make_channel(obf!("com.apple.instruments.server.services.screenshot"))
|
||||||
|
.await?; // Drop `&mut client` before continuing
|
||||||
|
|
||||||
|
Ok(Self { channel })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take screenshot from the device
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(Vec<u8>)` - the bytes of the screenshot
|
||||||
|
/// * `Err(IdeviceError)` - If communication fails
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// * `IdeviceError::UnexpectedResponse` if server response is invalid
|
||||||
|
/// * Other communication or serialization errors
|
||||||
|
pub async fn take_screenshot(&mut self) -> Result<Vec<u8>, IdeviceError> {
|
||||||
|
let method = Value::String("takeScreenshot".into());
|
||||||
|
|
||||||
|
self.channel.call_method(Some(method), None, true).await?;
|
||||||
|
|
||||||
|
let msg = self.channel.read_message().await?;
|
||||||
|
match msg.data {
|
||||||
|
Some(Value::Data(data)) => Ok(data),
|
||||||
|
_ => Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
idevice/src/services/mobileactivationd.rs
Normal file
91
idevice/src/services/mobileactivationd.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
//! mobileactivationd activates iOS devices.
|
||||||
|
//! This isn't a normal service, as it requires a new connection for each request.
|
||||||
|
//! As such, this service requires a provider itself, instead of temporary usage of one.
|
||||||
|
|
||||||
|
use plist::Dictionary;
|
||||||
|
|
||||||
|
use crate::{Idevice, IdeviceError, IdeviceService, lockdown::LockdownClient, obf};
|
||||||
|
|
||||||
|
pub struct MobileActivationdClient<'a> {
|
||||||
|
provider: &'a dyn crate::provider::IdeviceProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal structure for temporary service connections.
|
||||||
|
/// This struct exists to take advantage of the service trait.
|
||||||
|
struct MobileActivationdInternal {
|
||||||
|
pub idevice: Idevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdeviceService for MobileActivationdInternal {
|
||||||
|
/// Returns the service name as registered with lockdownd
|
||||||
|
fn service_name() -> std::borrow::Cow<'static, str> {
|
||||||
|
obf!("com.apple.mobileactivationd")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
|
||||||
|
Ok(Self::new(idevice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileActivationdInternal {
|
||||||
|
fn new(idevice: Idevice) -> Self {
|
||||||
|
Self { idevice }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MobileActivationdClient<'a> {
|
||||||
|
pub fn new(provider: &'a dyn crate::provider::IdeviceProvider) -> Self {
|
||||||
|
Self { provider }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn state(&self) -> Result<String, IdeviceError> {
|
||||||
|
if let Ok(res) = self.send_command("GetActivationStateRequest", None).await
|
||||||
|
&& let Some(v) = res.get("Value").and_then(|x| x.as_string())
|
||||||
|
{
|
||||||
|
Ok(v.to_string())
|
||||||
|
} else {
|
||||||
|
let mut lc = LockdownClient::connect(self.provider).await?;
|
||||||
|
lc.start_session(&self.provider.get_pairing_file().await?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let res = lc.get_value(Some("ActivationState"), None).await?;
|
||||||
|
if let Some(v) = res.as_string() {
|
||||||
|
Ok(v.to_string())
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn activated(&self) -> Result<bool, IdeviceError> {
|
||||||
|
Ok(self.state().await? == "Activated")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivates the device.
|
||||||
|
/// Protocol gives no response on whether it worked or not, so good luck
|
||||||
|
pub async fn deactivate(&self) -> Result<(), IdeviceError> {
|
||||||
|
self.send_command("DeactivateRequest", None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_command(
|
||||||
|
&self,
|
||||||
|
command: impl Into<String>,
|
||||||
|
value: Option<&str>,
|
||||||
|
) -> Result<Dictionary, IdeviceError> {
|
||||||
|
let mut service = self.service_connect().await?;
|
||||||
|
let command = command.into();
|
||||||
|
let req = crate::plist!({
|
||||||
|
"Command": command,
|
||||||
|
"Value":? value,
|
||||||
|
});
|
||||||
|
service.send_plist(req).await?;
|
||||||
|
service.read_plist().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn service_connect(&self) -> Result<Idevice, IdeviceError> {
|
||||||
|
Ok(MobileActivationdInternal::connect(self.provider)
|
||||||
|
.await?
|
||||||
|
.idevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ pub mod lockdown;
|
|||||||
pub mod misagent;
|
pub mod misagent;
|
||||||
#[cfg(feature = "mobile_image_mounter")]
|
#[cfg(feature = "mobile_image_mounter")]
|
||||||
pub mod mobile_image_mounter;
|
pub mod mobile_image_mounter;
|
||||||
|
#[cfg(feature = "mobileactivationd")]
|
||||||
|
pub mod mobileactivationd;
|
||||||
#[cfg(feature = "mobilebackup2")]
|
#[cfg(feature = "mobilebackup2")]
|
||||||
pub mod mobilebackup2;
|
pub mod mobilebackup2;
|
||||||
#[cfg(feature = "syslog_relay")]
|
#[cfg(feature = "syslog_relay")]
|
||||||
@@ -43,7 +45,12 @@ pub mod remote_pairing;
|
|||||||
pub mod restore_service;
|
pub mod restore_service;
|
||||||
#[cfg(feature = "rsd")]
|
#[cfg(feature = "rsd")]
|
||||||
pub mod rsd;
|
pub mod rsd;
|
||||||
|
#[cfg(feature = "screenshotr")]
|
||||||
|
pub mod screenshotr;
|
||||||
#[cfg(feature = "springboardservices")]
|
#[cfg(feature = "springboardservices")]
|
||||||
pub mod springboardservices;
|
pub mod springboardservices;
|
||||||
#[cfg(feature = "syslog_relay")]
|
#[cfg(feature = "syslog_relay")]
|
||||||
pub mod syslog_relay;
|
pub mod syslog_relay;
|
||||||
|
|
||||||
|
#[cfg(feature = "location_simulation")]
|
||||||
|
pub mod simulate_location;
|
||||||
|
|||||||
123
idevice/src/services/screenshotr.rs
Normal file
123
idevice/src/services/screenshotr.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//! iOS screenshotr service client
|
||||||
|
//!
|
||||||
|
//! Provides functionality for interacting with the screenshot service on iOS devices below iOS 17,
|
||||||
|
//! which allows taking screenshots.
|
||||||
|
|
||||||
|
use crate::{Idevice, IdeviceError, IdeviceService, obf};
|
||||||
|
use log::{debug, warn};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
pub struct ScreenshotService {
|
||||||
|
/// Underlying device connection
|
||||||
|
pub idevice: Idevice,
|
||||||
|
}
|
||||||
|
impl IdeviceService for ScreenshotService {
|
||||||
|
fn service_name() -> Cow<'static, str> {
|
||||||
|
obf!("com.apple.mobile.screenshotr")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_stream(idevice: Idevice) -> Result<Self, IdeviceError> {
|
||||||
|
let mut client = Self::new(idevice);
|
||||||
|
// Perform DeviceLink handshake first
|
||||||
|
client.dl_version_exchange().await?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenshotService {
|
||||||
|
pub fn new(idevice: Idevice) -> Self {
|
||||||
|
Self { idevice }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
debug!("Starting DeviceLink version exchange");
|
||||||
|
// 1) Receive DLMessageVersionExchange
|
||||||
|
let (msg, _arr) = self.receive_dl_message().await?;
|
||||||
|
if msg != "DLMessageVersionExchange" {
|
||||||
|
warn!("Expected DLMessageVersionExchange, got {msg}");
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Send DLVersionsOk with version 400
|
||||||
|
let out = vec![
|
||||||
|
plist::Value::String("DLMessageVersionExchange".into()),
|
||||||
|
plist::Value::String("DLVersionsOk".into()),
|
||||||
|
plist::Value::Integer(400u64.into()),
|
||||||
|
];
|
||||||
|
self.send_dl_array(out).await?;
|
||||||
|
|
||||||
|
// 3) Receive DLMessageDeviceReady
|
||||||
|
let (msg2, _arr2) = self.receive_dl_message().await?;
|
||||||
|
if msg2 != "DLMessageDeviceReady" {
|
||||||
|
warn!("Expected DLMessageDeviceReady, got {msg2}");
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a raw DL array as binary plist
|
||||||
|
async fn send_dl_array(&mut self, array: Vec<plist::Value>) -> Result<(), IdeviceError> {
|
||||||
|
self.idevice.send_bplist(plist::Value::Array(array)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives any DL* message and returns (message_tag, full_array_value)
|
||||||
|
pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> {
|
||||||
|
if let Some(socket) = &mut self.idevice.socket {
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
socket.read_exact(&mut buf).await?;
|
||||||
|
let len = u32::from_be_bytes(buf);
|
||||||
|
let mut body = vec![0; len as usize];
|
||||||
|
socket.read_exact(&mut body).await?;
|
||||||
|
let value: plist::Value = plist::from_bytes(&body)?;
|
||||||
|
if let plist::Value::Array(arr) = &value
|
||||||
|
&& let Some(plist::Value::String(tag)) = arr.first()
|
||||||
|
{
|
||||||
|
return Ok((tag.clone(), value));
|
||||||
|
}
|
||||||
|
warn!("Invalid DL message format");
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::NoEstablishedConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn take_screenshot(&mut self) -> Result<Vec<u8>, IdeviceError> {
|
||||||
|
// Send DLMessageTakeScreenshot
|
||||||
|
|
||||||
|
let message_type_dict = crate::plist!(dict {
|
||||||
|
"MessageType": "ScreenShotRequest"
|
||||||
|
});
|
||||||
|
|
||||||
|
let out = vec![
|
||||||
|
plist::Value::String("DLMessageProcessMessage".into()),
|
||||||
|
plist::Value::Dictionary(message_type_dict),
|
||||||
|
];
|
||||||
|
self.send_dl_array(out).await?;
|
||||||
|
|
||||||
|
// Receive DLMessageScreenshotData
|
||||||
|
let (msg, value) = self.receive_dl_message().await?;
|
||||||
|
if msg != "DLMessageProcessMessage" {
|
||||||
|
warn!("Expected DLMessageProcessMessage, got {msg}");
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let plist::Value::Array(arr) = &value
|
||||||
|
&& arr.len() == 2
|
||||||
|
{
|
||||||
|
if let Some(plist::Value::Dictionary(dict)) = arr.get(1) {
|
||||||
|
if let Some(plist::Value::Data(data)) = dict.get("ScreenShotData") {
|
||||||
|
Ok(data.clone())
|
||||||
|
} else {
|
||||||
|
warn!("Invalid ScreenShotData format");
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Invalid DLMessageScreenshotData format");
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Invalid DLMessageScreenshotData format");
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
idevice/src/services/simulate_location.rs
Normal file
38
idevice/src/services/simulate_location.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::{Idevice, IdeviceError, IdeviceService, obf};
|
||||||
|
|
||||||
|
pub struct LocationSimulationService {
|
||||||
|
idevice: Idevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdeviceService for LocationSimulationService {
|
||||||
|
fn service_name() -> std::borrow::Cow<'static, str> {
|
||||||
|
obf!("com.apple.dt.simulatelocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_stream(idevice: Idevice) -> Result<Self, IdeviceError> {
|
||||||
|
Ok(Self::new(idevice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocationSimulationService {
|
||||||
|
pub fn new(idevice: Idevice) -> Self {
|
||||||
|
Self { idevice }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
let message: [u8; 4] = [0x00, 0x00, 0x00, 0x01];
|
||||||
|
self.idevice.send_raw(&message).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set(&mut self, latitude: &str, longtiude: &str) -> Result<(), IdeviceError> {
|
||||||
|
let message: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
|
||||||
|
let latitude_len = latitude.len() as u32;
|
||||||
|
let longtiude_len = longtiude.len() as u32;
|
||||||
|
let latitude_bytes = [&latitude_len.to_be_bytes(), latitude.as_bytes()].concat();
|
||||||
|
let longitude_bytes = [&longtiude_len.to_be_bytes(), longtiude.as_bytes()].concat();
|
||||||
|
let data = [&message[..], &latitude_bytes[..], &longitude_bytes[..]].concat();
|
||||||
|
self.idevice.send_raw(data.as_slice()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
//! High-level install/upgrade helpers
|
|
||||||
//!
|
|
||||||
//! This module provides convenient wrappers that mirror ideviceinstaller's
|
|
||||||
//! behavior by uploading a package to `PublicStaging` via AFC and then
|
|
||||||
//! issuing `Install`/`Upgrade` commands through InstallationProxy.
|
|
||||||
//!
|
|
||||||
//! Notes:
|
|
||||||
//! - The package path used by InstallationProxy must be a path inside the
|
|
||||||
//! AFC jail (e.g. `PublicStaging/<name>`)
|
|
||||||
//! - For `.ipa` files, we upload the whole file to `PublicStaging/<file_name>`
|
|
||||||
//! - For directories (developer bundles), we recursively mirror the directory
|
|
||||||
//! into `PublicStaging/<dir_name>` and pass that directory path.
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
IdeviceError, IdeviceService,
|
|
||||||
provider::IdeviceProvider,
|
|
||||||
services::{
|
|
||||||
afc::{AfcClient, opcode::AfcFopenMode},
|
|
||||||
installation_proxy::InstallationProxyClient,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const PUBLIC_STAGING: &str = "PublicStaging";
|
|
||||||
|
|
||||||
/// Result of a prepared upload, containing the remote path to use in Install/Upgrade
|
|
||||||
struct UploadedPackageInfo {
|
|
||||||
/// Path inside the AFC jail for InstallationProxy `PackagePath`
|
|
||||||
remote_package_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure `PublicStaging` exists on device via AFC
|
|
||||||
async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> {
|
|
||||||
// Try to stat and if it fails, create directory
|
|
||||||
match afc.get_file_info(PUBLIC_STAGING).await {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_) => afc.mk_dir(PUBLIC_STAGING).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload a single file to a destination path on device using AFC
|
|
||||||
async fn afc_upload_file(
|
|
||||||
afc: &mut AfcClient,
|
|
||||||
local_path: &Path,
|
|
||||||
remote_path: &str,
|
|
||||||
) -> Result<(), IdeviceError> {
|
|
||||||
let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?;
|
|
||||||
let bytes = tokio::fs::read(local_path).await?;
|
|
||||||
fd.write(&bytes).await?;
|
|
||||||
fd.close().await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively upload a directory to device via AFC (mirror contents)
|
|
||||||
async fn afc_upload_dir(
|
|
||||||
afc: &mut AfcClient,
|
|
||||||
local_dir: &Path,
|
|
||||||
remote_dir: &str,
|
|
||||||
) -> Result<(), IdeviceError> {
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
afc.mk_dir(remote_dir).await.ok();
|
|
||||||
|
|
||||||
let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new();
|
|
||||||
queue.push_back((local_dir.to_path_buf(), remote_dir.to_string()));
|
|
||||||
|
|
||||||
while let Some((cur_local, cur_remote)) = queue.pop_front() {
|
|
||||||
let mut rd = tokio::fs::read_dir(&cur_local).await?;
|
|
||||||
while let Some(entry) = rd.next_entry().await? {
|
|
||||||
let meta = entry.metadata().await?;
|
|
||||||
let name = entry.file_name();
|
|
||||||
let name = name.to_string_lossy().into_owned();
|
|
||||||
if name == "." || name == ".." {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let child_local = entry.path();
|
|
||||||
let child_remote = format!("{}/{}", cur_remote, name);
|
|
||||||
if meta.is_dir() {
|
|
||||||
afc.mk_dir(&child_remote).await.ok();
|
|
||||||
queue.push_back((child_local, child_remote));
|
|
||||||
} else if meta.is_file() {
|
|
||||||
afc_upload_file(afc, &child_local, &child_remote).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload a package to `PublicStaging` and return its InstallationProxy path
|
|
||||||
///
|
|
||||||
/// - If `local_path` is a file, it will be uploaded to `PublicStaging/<name>`
|
|
||||||
/// - If it is a directory, it will be mirrored to `PublicStaging/<dir_name>`
|
|
||||||
async fn upload_package_to_public_staging<P: AsRef<Path>>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
local_path: P,
|
|
||||||
) -> Result<UploadedPackageInfo, IdeviceError> {
|
|
||||||
// Connect to AFC via the generic service connector
|
|
||||||
let mut afc = AfcClient::connect(provider).await?;
|
|
||||||
|
|
||||||
ensure_public_staging(&mut afc).await?;
|
|
||||||
|
|
||||||
let local_path = local_path.as_ref();
|
|
||||||
let file_name: String = local_path
|
|
||||||
.file_name()
|
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
|
||||||
.ok_or_else(|| IdeviceError::InvalidArgument)?;
|
|
||||||
let remote_path = format!("{}/{}", PUBLIC_STAGING, file_name);
|
|
||||||
|
|
||||||
let meta = tokio::fs::metadata(local_path).await?;
|
|
||||||
if meta.is_dir() {
|
|
||||||
afc_upload_dir(&mut afc, local_path, &remote_path).await?;
|
|
||||||
} else {
|
|
||||||
afc_upload_file(&mut afc, local_path, &remote_path).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(UploadedPackageInfo {
|
|
||||||
remote_package_path: remote_path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install an application by first uploading the local package and then invoking InstallationProxy.
|
|
||||||
///
|
|
||||||
/// - Accepts a local file path or directory path.
|
|
||||||
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
|
||||||
pub async fn install_package<P: AsRef<Path>>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
local_path: P,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
) -> Result<(), IdeviceError> {
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_package_to_public_staging(provider, local_path).await?;
|
|
||||||
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.install(remote_package_path, options).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upgrade an application by first uploading the local package and then invoking InstallationProxy.
|
|
||||||
///
|
|
||||||
/// - Accepts a local file path or directory path.
|
|
||||||
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
|
||||||
pub async fn upgrade_package<P: AsRef<Path>>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
local_path: P,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
) -> Result<(), IdeviceError> {
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_package_to_public_staging(provider, local_path).await?;
|
|
||||||
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.upgrade(remote_package_path, options).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `install_package` but providing a callback that receives `(percent_complete, state)`
|
|
||||||
/// updates while InstallationProxy performs the operation.
|
|
||||||
pub async fn install_package_with_callback<P: AsRef<Path>, Fut, S>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
local_path: P,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
callback: impl Fn((u64, S)) -> Fut,
|
|
||||||
state: S,
|
|
||||||
) -> Result<(), IdeviceError>
|
|
||||||
where
|
|
||||||
Fut: std::future::Future<Output = ()>,
|
|
||||||
S: Clone,
|
|
||||||
{
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_package_to_public_staging(provider, local_path).await?;
|
|
||||||
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.install_with_callback(remote_package_path, options, callback, state)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)`
|
|
||||||
/// updates while InstallationProxy performs the operation.
|
|
||||||
pub async fn upgrade_package_with_callback<P: AsRef<Path>, Fut, S>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
local_path: P,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
callback: impl Fn((u64, S)) -> Fut,
|
|
||||||
state: S,
|
|
||||||
) -> Result<(), IdeviceError>
|
|
||||||
where
|
|
||||||
Fut: std::future::Future<Output = ()>,
|
|
||||||
S: Clone,
|
|
||||||
{
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_package_to_public_staging(provider, local_path).await?;
|
|
||||||
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.upgrade_with_callback(remote_package_path, options, callback, state)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload raw bytes to `PublicStaging/<remote_name>` via AFC and return the remote package path.
|
|
||||||
///
|
|
||||||
/// - This is useful when the package is not present on disk or is generated in-memory.
|
|
||||||
async fn upload_bytes_to_public_staging(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
data: impl AsRef<[u8]>,
|
|
||||||
remote_name: &str,
|
|
||||||
) -> Result<UploadedPackageInfo, IdeviceError> {
|
|
||||||
// Connect to AFC
|
|
||||||
let mut afc = AfcClient::connect(provider).await?;
|
|
||||||
ensure_public_staging(&mut afc).await?;
|
|
||||||
|
|
||||||
let remote_path = format!("{}/{}", PUBLIC_STAGING, remote_name);
|
|
||||||
let mut fd = afc.open(&remote_path, AfcFopenMode::WrOnly).await?;
|
|
||||||
fd.write(data.as_ref()).await?;
|
|
||||||
fd.close().await?;
|
|
||||||
|
|
||||||
Ok(UploadedPackageInfo {
|
|
||||||
remote_package_path: remote_path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install an application from raw bytes by first uploading them to `PublicStaging` and then
|
|
||||||
/// invoking InstallationProxy `Install`.
|
|
||||||
///
|
|
||||||
/// - `remote_name` determines the remote filename under `PublicStaging`.
|
|
||||||
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
|
||||||
pub async fn install_bytes(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
data: impl AsRef<[u8]>,
|
|
||||||
remote_name: &str,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
) -> Result<(), IdeviceError> {
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.install(remote_package_path, options).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)`
|
|
||||||
/// updates while InstallationProxy performs the install operation.
|
|
||||||
///
|
|
||||||
/// Tip:
|
|
||||||
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
|
|
||||||
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
|
|
||||||
pub async fn install_bytes_with_callback<Fut, S>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
data: impl AsRef<[u8]>,
|
|
||||||
remote_name: &str,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
callback: impl Fn((u64, S)) -> Fut,
|
|
||||||
state: S,
|
|
||||||
) -> Result<(), IdeviceError>
|
|
||||||
where
|
|
||||||
Fut: std::future::Future<Output = ()>,
|
|
||||||
S: Clone,
|
|
||||||
{
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.install_with_callback(remote_package_path, options, callback, state)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then
|
|
||||||
/// invoking InstallationProxy `Upgrade`.
|
|
||||||
///
|
|
||||||
/// - `remote_name` determines the remote filename under `PublicStaging`.
|
|
||||||
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
|
||||||
pub async fn upgrade_bytes(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
data: impl AsRef<[u8]>,
|
|
||||||
remote_name: &str,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
) -> Result<(), IdeviceError> {
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.upgrade(remote_package_path, options).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)`
|
|
||||||
/// updates while InstallationProxy performs the upgrade operation.
|
|
||||||
///
|
|
||||||
/// Tip:
|
|
||||||
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
|
|
||||||
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
|
|
||||||
pub async fn upgrade_bytes_with_callback<Fut, S>(
|
|
||||||
provider: &dyn IdeviceProvider,
|
|
||||||
data: impl AsRef<[u8]>,
|
|
||||||
remote_name: &str,
|
|
||||||
options: Option<plist::Value>,
|
|
||||||
callback: impl Fn((u64, S)) -> Fut,
|
|
||||||
state: S,
|
|
||||||
) -> Result<(), IdeviceError>
|
|
||||||
where
|
|
||||||
Fut: std::future::Future<Output = ()>,
|
|
||||||
S: Clone,
|
|
||||||
{
|
|
||||||
let UploadedPackageInfo {
|
|
||||||
remote_package_path,
|
|
||||||
} = upload_bytes_to_public_staging(provider, data, remote_name).await?;
|
|
||||||
let mut inst = InstallationProxyClient::connect(provider).await?;
|
|
||||||
inst.upgrade_with_callback(remote_package_path, options, callback, state)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
273
idevice/src/utils/installation/helpers.rs
Normal file
273
idevice/src/utils/installation/helpers.rs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
use std::{io::Cursor, path::Path};
|
||||||
|
|
||||||
|
use async_zip::base::read::seek::ZipFileReader;
|
||||||
|
use futures::AsyncReadExt as _;
|
||||||
|
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
IdeviceError, IdeviceService,
|
||||||
|
afc::{AfcClient, opcode::AfcFopenMode},
|
||||||
|
plist,
|
||||||
|
provider::IdeviceProvider,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PUBLIC_STAGING: &str = "PublicStaging";
|
||||||
|
|
||||||
|
pub const IPCC_REMOTE_FILE: &str = "idevice.ipcc";
|
||||||
|
|
||||||
|
pub const IPA_REMOTE_FILE: &str = "idevice.ipa";
|
||||||
|
|
||||||
|
/// Result of a prepared upload, containing the remote path to use in Install/Upgrade
|
||||||
|
pub struct InstallPackage {
|
||||||
|
/// Path inside the AFC jail for InstallationProxy `PackagePath`
|
||||||
|
pub remote_package_path: String,
|
||||||
|
|
||||||
|
// Each package type has a special option that has to be passed
|
||||||
|
pub options: plist::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represent the type of package being installed.
|
||||||
|
pub enum PackageType {
|
||||||
|
Ipcc, // Carrier bundle package
|
||||||
|
// an IPA package needs the build id to be installed
|
||||||
|
Ipa(String), // iOS app package
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageType {
|
||||||
|
pub fn get_remote_file(&self) -> Result<&'static str, IdeviceError> {
|
||||||
|
match self {
|
||||||
|
Self::Ipcc => Ok(IPCC_REMOTE_FILE),
|
||||||
|
Self::Ipa(_) => Ok(IPA_REMOTE_FILE),
|
||||||
|
Self::Unknown => Err(IdeviceError::InstallationProxyOperationFailed(
|
||||||
|
"invalid package".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure `PublicStaging` exists on device via AFC
|
||||||
|
pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> {
|
||||||
|
// Try to stat and if it fails, create directory
|
||||||
|
match afc.get_file_info(PUBLIC_STAGING).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => afc.mk_dir(PUBLIC_STAGING).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the bundle id of a package by looping through it's files and looking inside of the
|
||||||
|
// `Info.plist`
|
||||||
|
pub async fn get_bundle_id<T>(file: &mut T) -> Result<String, IdeviceError>
|
||||||
|
where
|
||||||
|
T: AsyncBufRead + AsyncSeek + Unpin,
|
||||||
|
{
|
||||||
|
let mut zip_file = ZipFileReader::with_tokio(file).await?;
|
||||||
|
|
||||||
|
for i in 0..zip_file.file().entries().len() {
|
||||||
|
let mut entry_reader = zip_file.reader_with_entry(i).await?;
|
||||||
|
let entry = entry_reader.entry();
|
||||||
|
|
||||||
|
let inner_file_path = entry
|
||||||
|
.filename()
|
||||||
|
.as_str()
|
||||||
|
.map_err(|_| IdeviceError::Utf8Error)?
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
|
let path_segments_count = inner_file_path.split('/').count();
|
||||||
|
|
||||||
|
// there's multiple `Info.plist` files, we only need the one that's in the root of the
|
||||||
|
// package
|
||||||
|
//
|
||||||
|
// 1 2 3
|
||||||
|
// which is in this case: Playload -> APP_NAME.app -> Info.plist
|
||||||
|
if inner_file_path.ends_with("Info.plist") && path_segments_count == 3 {
|
||||||
|
let mut info_plist_bytes = Vec::new();
|
||||||
|
entry_reader.read_to_end(&mut info_plist_bytes).await?;
|
||||||
|
|
||||||
|
let info_plist: plist::Value = plist::from_bytes(&info_plist_bytes)?;
|
||||||
|
|
||||||
|
if let Some(bundle_id) = info_plist
|
||||||
|
.as_dictionary()
|
||||||
|
.and_then(|dict| dict.get("CFBundleIdentifier"))
|
||||||
|
.and_then(|v| v.as_string())
|
||||||
|
{
|
||||||
|
return Ok(bundle_id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(IdeviceError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the type of package based on its content (IPA or IPCC).
|
||||||
|
pub async fn determine_package_type<P: AsRef<[u8]>>(
|
||||||
|
package: &P,
|
||||||
|
) -> Result<PackageType, IdeviceError> {
|
||||||
|
let mut package_cursor = BufReader::new(Cursor::new(package.as_ref()));
|
||||||
|
|
||||||
|
let mut archive = ZipFileReader::with_tokio(&mut package_cursor).await?;
|
||||||
|
|
||||||
|
// the first index is the first folder name, which is probably `Payload`
|
||||||
|
//
|
||||||
|
// we need the folder inside of that `Payload`, which has an extension that we can
|
||||||
|
// determine the type of the package from it, hence the second index
|
||||||
|
let inside_folder = archive.reader_with_entry(1).await?;
|
||||||
|
|
||||||
|
let folder_name = inside_folder
|
||||||
|
.entry()
|
||||||
|
.filename()
|
||||||
|
.as_str()
|
||||||
|
.map_err(|_| IdeviceError::Utf8Error)?
|
||||||
|
.split('/')
|
||||||
|
.nth(1)
|
||||||
|
// only if the package does not have anything inside of the `Payload` folder
|
||||||
|
.ok_or(async_zip::error::ZipError::EntryIndexOutOfBounds)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let bundle_id = get_bundle_id(&mut package_cursor).await?;
|
||||||
|
|
||||||
|
if folder_name.ends_with(".bundle") {
|
||||||
|
Ok(PackageType::Ipcc)
|
||||||
|
} else if folder_name.ends_with(".app") {
|
||||||
|
Ok(PackageType::Ipa(bundle_id))
|
||||||
|
} else {
|
||||||
|
Ok(PackageType::Unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a single file to a destination path on device using AFC
|
||||||
|
pub async fn afc_upload_file<F: AsRef<[u8]>>(
|
||||||
|
afc: &mut AfcClient,
|
||||||
|
file: F,
|
||||||
|
remote_path: &str,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?;
|
||||||
|
fd.write(file.as_ref()).await?;
|
||||||
|
fd.close().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively upload a directory to device via AFC (mirror contents)
|
||||||
|
pub async fn afc_upload_dir(
|
||||||
|
afc: &mut AfcClient,
|
||||||
|
local_dir: &Path,
|
||||||
|
remote_dir: &str,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
afc.mk_dir(remote_dir).await.ok();
|
||||||
|
|
||||||
|
let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new();
|
||||||
|
queue.push_back((local_dir.to_path_buf(), remote_dir.to_string()));
|
||||||
|
|
||||||
|
while let Some((cur_local, cur_remote)) = queue.pop_front() {
|
||||||
|
let mut rd = tokio::fs::read_dir(&cur_local).await?;
|
||||||
|
while let Some(entry) = rd.next_entry().await? {
|
||||||
|
let meta = entry.metadata().await?;
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name = name.to_string_lossy().into_owned();
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let child_local = entry.path();
|
||||||
|
let child_remote = format!("{cur_remote}/{name}");
|
||||||
|
if meta.is_dir() {
|
||||||
|
afc.mk_dir(&child_remote).await.ok();
|
||||||
|
queue.push_back((child_local, child_remote));
|
||||||
|
} else if meta.is_file() {
|
||||||
|
afc_upload_file(afc, tokio::fs::read(&child_local).await?, &child_remote).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a file to `PublicStaging` and return its InstallationProxy path
|
||||||
|
async fn upload_file_to_public_staging<P: AsRef<[u8]>>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
file: P,
|
||||||
|
) -> Result<InstallPackage, IdeviceError> {
|
||||||
|
// Connect to AFC via the generic service connector
|
||||||
|
let mut afc = AfcClient::connect(provider).await?;
|
||||||
|
|
||||||
|
ensure_public_staging(&mut afc).await?;
|
||||||
|
|
||||||
|
let file = file.as_ref();
|
||||||
|
|
||||||
|
let package_type = determine_package_type(&file).await?;
|
||||||
|
|
||||||
|
let remote_path = format!("{PUBLIC_STAGING}/{}", package_type.get_remote_file()?);
|
||||||
|
|
||||||
|
afc_upload_file(&mut afc, file, &remote_path).await?;
|
||||||
|
|
||||||
|
let options = match package_type {
|
||||||
|
PackageType::Ipcc => plist!({"PackageType": "CarrierBundle"}),
|
||||||
|
PackageType::Ipa(build_id) => plist!({"CFBundleIdentifier": build_id}),
|
||||||
|
PackageType::Unknown => plist!({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(InstallPackage {
|
||||||
|
remote_package_path: remote_path,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively Upload a directory of file to `PublicStaging`
|
||||||
|
async fn upload_dir_to_public_staging<P: AsRef<Path>>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
file: P,
|
||||||
|
) -> Result<InstallPackage, IdeviceError> {
|
||||||
|
let mut afc = AfcClient::connect(provider).await?;
|
||||||
|
|
||||||
|
ensure_public_staging(&mut afc).await?;
|
||||||
|
|
||||||
|
let file = file.as_ref();
|
||||||
|
|
||||||
|
let remote_path = format!("{PUBLIC_STAGING}/{IPA_REMOTE_FILE}");
|
||||||
|
|
||||||
|
afc_upload_dir(&mut afc, file, &remote_path).await?;
|
||||||
|
|
||||||
|
Ok(InstallPackage {
|
||||||
|
remote_package_path: remote_path,
|
||||||
|
options: plist!({"PackageType": "Developer"}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_file_upload(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
caller_options: Option<plist::Value>,
|
||||||
|
) -> Result<InstallPackage, IdeviceError> {
|
||||||
|
let InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options,
|
||||||
|
} = upload_file_to_public_staging(provider, data).await?;
|
||||||
|
let full_options = plist!({
|
||||||
|
:<? caller_options,
|
||||||
|
:< options,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options: full_options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_dir_upload(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
local_path: impl AsRef<Path>,
|
||||||
|
caller_options: Option<plist::Value>,
|
||||||
|
) -> Result<InstallPackage, IdeviceError> {
|
||||||
|
let InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options,
|
||||||
|
} = upload_dir_to_public_staging(provider, &local_path).await?;
|
||||||
|
|
||||||
|
let full_options = plist!({
|
||||||
|
:<? caller_options,
|
||||||
|
:< options,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options: full_options,
|
||||||
|
})
|
||||||
|
}
|
||||||
186
idevice/src/utils/installation/mod.rs
Normal file
186
idevice/src/utils/installation/mod.rs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
//! High-level install/upgrade helpers
|
||||||
|
//!
|
||||||
|
//! This module provides convenient wrappers that mirror ideviceinstaller's
|
||||||
|
//! behavior by uploading a package to `PublicStaging` via AFC and then
|
||||||
|
//! issuing `Install`/`Upgrade` commands through InstallationProxy.
|
||||||
|
//!
|
||||||
|
//! Notes:
|
||||||
|
//! - The package path used by InstallationProxy must be a path inside the
|
||||||
|
//! AFC jail (e.g. `PublicStaging/<name>`)
|
||||||
|
//! - For `.ipa` files, we upload the whole file to `PublicStaging/<file_name>`
|
||||||
|
//! - For directories (developer bundles), we recursively mirror the directory
|
||||||
|
//! into `PublicStaging/<dir_name>` and pass that directory path.
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use helpers::{InstallPackage, prepare_dir_upload, prepare_file_upload};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
IdeviceError, IdeviceService, provider::IdeviceProvider,
|
||||||
|
services::installation_proxy::InstallationProxyClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Install an application by first uploading the local package and then invoking InstallationProxy.
|
||||||
|
///
|
||||||
|
/// - Accepts a local file path or directory path.
|
||||||
|
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
||||||
|
pub async fn install_package<P: AsRef<Path>>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
local_path: P,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
install_package_with_callback(provider, local_path, options, |_| async {}, ()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `install_package` but providing a callback that receives `(percent_complete, state)`
|
||||||
|
/// updates while InstallationProxy performs the operation.
|
||||||
|
pub async fn install_package_with_callback<P: AsRef<Path>, Fut, S>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
local_path: P,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
callback: impl Fn((u64, S)) -> Fut,
|
||||||
|
state: S,
|
||||||
|
) -> Result<(), IdeviceError>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = ()>,
|
||||||
|
S: Clone,
|
||||||
|
{
|
||||||
|
let metadata = tokio::fs::metadata(&local_path).await?;
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
let InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options,
|
||||||
|
} = prepare_dir_upload(provider, local_path, options).await?;
|
||||||
|
let mut inst = InstallationProxyClient::connect(provider).await?;
|
||||||
|
|
||||||
|
inst.upgrade_with_callback(remote_package_path, Some(options), callback, state)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
let data = tokio::fs::read(&local_path).await?;
|
||||||
|
install_bytes_with_callback(provider, data, options, callback, state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upgrade an application by first uploading the local package and then invoking InstallationProxy.
|
||||||
|
///
|
||||||
|
/// - Accepts a local file path or directory path.
|
||||||
|
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
||||||
|
pub async fn upgrade_package<P: AsRef<Path>>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
local_path: P,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
upgrade_package_with_callback(provider, local_path, options, |_| async {}, ()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)`
|
||||||
|
/// updates while InstallationProxy performs the operation.
|
||||||
|
pub async fn upgrade_package_with_callback<P: AsRef<Path>, Fut, S>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
local_path: P,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
callback: impl Fn((u64, S)) -> Fut,
|
||||||
|
state: S,
|
||||||
|
) -> Result<(), IdeviceError>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = ()>,
|
||||||
|
S: Clone,
|
||||||
|
{
|
||||||
|
let metadata = tokio::fs::metadata(&local_path).await?;
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
let InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options,
|
||||||
|
} = prepare_dir_upload(provider, local_path, options).await?;
|
||||||
|
let mut inst = InstallationProxyClient::connect(provider).await?;
|
||||||
|
|
||||||
|
inst.upgrade_with_callback(remote_package_path, Some(options), callback, state)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
let data = tokio::fs::read(&local_path).await?;
|
||||||
|
upgrade_bytes_with_callback(provider, data, options, callback, state).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install an application from raw bytes by first uploading them to `PublicStaging` and then
|
||||||
|
/// invoking InstallationProxy `Install`.
|
||||||
|
///
|
||||||
|
/// - `remote_name` determines the remote filename under `PublicStaging`.
|
||||||
|
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
||||||
|
pub async fn install_bytes(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
install_bytes_with_callback(provider, data, options, |_| async {}, ()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)`
|
||||||
|
/// updates while InstallationProxy performs the install operation.
|
||||||
|
///
|
||||||
|
/// Tip:
|
||||||
|
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
|
||||||
|
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
|
||||||
|
pub async fn install_bytes_with_callback<Fut, S>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
callback: impl Fn((u64, S)) -> Fut,
|
||||||
|
state: S,
|
||||||
|
) -> Result<(), IdeviceError>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = ()>,
|
||||||
|
S: Clone,
|
||||||
|
{
|
||||||
|
let InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options,
|
||||||
|
} = prepare_file_upload(provider, data, options).await?;
|
||||||
|
let mut inst = InstallationProxyClient::connect(provider).await?;
|
||||||
|
|
||||||
|
inst.install_with_callback(remote_package_path, Some(options), callback, state)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then
|
||||||
|
/// invoking InstallationProxy `Upgrade`.
|
||||||
|
///
|
||||||
|
/// - `remote_name` determines the remote filename under `PublicStaging`.
|
||||||
|
/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults.
|
||||||
|
pub async fn upgrade_bytes(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
upgrade_bytes_with_callback(provider, data, options, |_| async {}, ()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)`
|
||||||
|
/// updates while InstallationProxy performs the upgrade operation.
|
||||||
|
///
|
||||||
|
/// Tip:
|
||||||
|
/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")`
|
||||||
|
/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`).
|
||||||
|
pub async fn upgrade_bytes_with_callback<Fut, S>(
|
||||||
|
provider: &dyn IdeviceProvider,
|
||||||
|
data: impl AsRef<[u8]>,
|
||||||
|
options: Option<plist::Value>,
|
||||||
|
callback: impl Fn((u64, S)) -> Fut,
|
||||||
|
state: S,
|
||||||
|
) -> Result<(), IdeviceError>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = ()>,
|
||||||
|
S: Clone,
|
||||||
|
{
|
||||||
|
let InstallPackage {
|
||||||
|
remote_package_path,
|
||||||
|
options,
|
||||||
|
} = prepare_file_upload(provider, data, options).await?;
|
||||||
|
let mut inst = InstallationProxyClient::connect(provider).await?;
|
||||||
|
|
||||||
|
inst.upgrade_with_callback(remote_package_path, Some(options), callback, state)
|
||||||
|
.await
|
||||||
|
}
|
||||||
2
justfile
2
justfile
@@ -6,7 +6,7 @@ check-features:
|
|||||||
ci-check: build-ffi-native build-tools-native build-cpp build-c
|
ci-check: build-ffi-native build-tools-native build-cpp build-c
|
||||||
cargo clippy --all-targets --all-features -- -D warnings
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
cargo fmt -- --check
|
cargo fmt -- --check
|
||||||
macos-ci-check: ci-check xcframework
|
macos-ci-check: ci-check
|
||||||
cd tools && cargo build --release --target x86_64-apple-darwin
|
cd tools && cargo build --release --target x86_64-apple-darwin
|
||||||
windows-ci-check: build-ffi-native build-tools-native build-cpp
|
windows-ci-check: build-ffi-native build-tools-native build-cpp
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,19 @@ path = "src/pcapd.rs"
|
|||||||
name = "preboard"
|
name = "preboard"
|
||||||
path = "src/preboard.rs"
|
path = "src/preboard.rs"
|
||||||
|
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "screenshot"
|
||||||
|
path = "src/screenshot.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "activation"
|
||||||
|
path = "src/activation.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "notifications"
|
||||||
|
path = "src/notifications.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
|
|||||||
113
tools/src/activation.rs
Normal file
113
tools/src/activation.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use idevice::{
|
||||||
|
IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let matches = Command::new("activation")
|
||||||
|
.about("mobileactivationd")
|
||||||
|
.arg(
|
||||||
|
Arg::new("host")
|
||||||
|
.long("host")
|
||||||
|
.value_name("HOST")
|
||||||
|
.help("IP address of the device"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("pairing_file")
|
||||||
|
.long("pairing-file")
|
||||||
|
.value_name("PATH")
|
||||||
|
.help("Path to the pairing file"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("udid")
|
||||||
|
.value_name("UDID")
|
||||||
|
.help("UDID of the device (overrides host/pairing file)")
|
||||||
|
.index(1),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("about")
|
||||||
|
.long("about")
|
||||||
|
.help("Show about information")
|
||||||
|
.action(clap::ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.subcommand(Command::new("state").about("Gets the activation state"))
|
||||||
|
.subcommand(Command::new("deactivate").about("Deactivates the device"))
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
if matches.get_flag("about") {
|
||||||
|
println!("activation - activate the device");
|
||||||
|
println!("Copyright (c) 2025 Jackson Coxson");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let udid = matches.get_one::<String>("udid");
|
||||||
|
let host = matches.get_one::<String>("host");
|
||||||
|
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||||
|
|
||||||
|
let provider = match common::get_provider(udid, host, pairing_file, "activation-jkcoxson").await
|
||||||
|
{
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let activation_client = MobileActivationdClient::new(&*provider);
|
||||||
|
let mut lc = LockdownClient::connect(&*provider)
|
||||||
|
.await
|
||||||
|
.expect("no lockdown");
|
||||||
|
lc.start_session(&provider.get_pairing_file().await.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("no TLS");
|
||||||
|
let udid = lc
|
||||||
|
.get_value(Some("UniqueDeviceID"), None)
|
||||||
|
.await
|
||||||
|
.expect("no udid")
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if matches.subcommand_matches("state").is_some() {
|
||||||
|
let s = activation_client.state().await.expect("no state");
|
||||||
|
println!("Activation State: {s}");
|
||||||
|
} else if matches.subcommand_matches("deactivate").is_some() {
|
||||||
|
println!("CAUTION: You are deactivating {udid}, press enter to continue.");
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input).ok();
|
||||||
|
activation_client.deactivate().await.expect("no deactivate");
|
||||||
|
// } else if matches.subcommand_matches("accept").is_some() {
|
||||||
|
// amfi_client
|
||||||
|
// .accept_developer_mode()
|
||||||
|
// .await
|
||||||
|
// .expect("Failed to show");
|
||||||
|
// } else if matches.subcommand_matches("status").is_some() {
|
||||||
|
// let status = amfi_client
|
||||||
|
// .get_developer_mode_status()
|
||||||
|
// .await
|
||||||
|
// .expect("Failed to get status");
|
||||||
|
// println!("Enabled: {status}");
|
||||||
|
// } else if let Some(matches) = matches.subcommand_matches("state") {
|
||||||
|
// let uuid: &String = match matches.get_one("uuid") {
|
||||||
|
// Some(u) => u,
|
||||||
|
// None => {
|
||||||
|
// eprintln!("No UUID passed. Invalid usage, pass -h for help");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// let status = amfi_client
|
||||||
|
// .trust_app_signer(uuid)
|
||||||
|
// .await
|
||||||
|
// .expect("Failed to get state");
|
||||||
|
// println!("Enabled: {status}");
|
||||||
|
} else {
|
||||||
|
eprintln!("Invalid usage, pass -h for help");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
use clap::{Arg, Command};
|
use clap::{Arg, Command};
|
||||||
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
||||||
|
|
||||||
|
use idevice::dvt::location_simulation::LocationSimulationClient;
|
||||||
|
use idevice::services::simulate_location::LocationSimulationService;
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -63,65 +65,103 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
|
||||||
|
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
|
||||||
|
let rsd_port = proxy.handshake.server_rsd_port;
|
||||||
|
|
||||||
|
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||||
|
let mut adapter = adapter.to_async_handle();
|
||||||
|
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
|
||||||
|
|
||||||
|
// Make the connection to RemoteXPC
|
||||||
|
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
||||||
|
|
||||||
|
let mut ls_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd(
|
||||||
|
&mut adapter,
|
||||||
|
&mut handshake,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("Failed to connect");
|
||||||
let rsd_port = proxy.handshake.server_rsd_port;
|
ls_client.read_message(0).await.expect("no read??");
|
||||||
|
let mut ls_client = LocationSimulationClient::new(&mut ls_client)
|
||||||
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
|
||||||
let mut adapter = adapter.to_async_handle();
|
|
||||||
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
|
|
||||||
|
|
||||||
// Make the connection to RemoteXPC
|
|
||||||
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
|
||||||
|
|
||||||
let mut ls_client =
|
|
||||||
idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake)
|
|
||||||
.await
|
|
||||||
.expect("Failed to connect");
|
|
||||||
ls_client.read_message(0).await.expect("no read??");
|
|
||||||
|
|
||||||
let mut ls_client =
|
|
||||||
idevice::dvt::location_simulation::LocationSimulationClient::new(&mut ls_client)
|
|
||||||
.await
|
.await
|
||||||
.expect("Unable to get channel for location simulation");
|
.expect("Unable to get channel for location simulation");
|
||||||
|
if matches.subcommand_matches("clear").is_some() {
|
||||||
if matches.subcommand_matches("clear").is_some() {
|
ls_client.clear().await.expect("Unable to clear");
|
||||||
ls_client.clear().await.expect("Unable to clear");
|
println!("Location cleared!");
|
||||||
println!("Location cleared!");
|
} else if let Some(matches) = matches.subcommand_matches("set") {
|
||||||
} else if let Some(matches) = matches.subcommand_matches("set") {
|
let latitude: &String = match matches.get_one("latitude") {
|
||||||
let latitude: &String = match matches.get_one("latitude") {
|
Some(l) => l,
|
||||||
Some(l) => l,
|
None => {
|
||||||
None => {
|
eprintln!("No latitude passed! Pass -h for help");
|
||||||
eprintln!("No latitude passed! Pass -h for help");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
};
|
||||||
};
|
let latitude: f64 = latitude.parse().expect("Failed to parse as float");
|
||||||
let latitude: f64 = latitude.parse().expect("Failed to parse as float");
|
let longitude: &String = match matches.get_one("longitude") {
|
||||||
let longitude: &String = match matches.get_one("longitude") {
|
Some(l) => l,
|
||||||
Some(l) => l,
|
None => {
|
||||||
None => {
|
eprintln!("No longitude passed! Pass -h for help");
|
||||||
eprintln!("No longitude passed! Pass -h for help");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
};
|
||||||
};
|
let longitude: f64 = longitude.parse().expect("Failed to parse as float");
|
||||||
let longitude: f64 = longitude.parse().expect("Failed to parse as float");
|
|
||||||
ls_client
|
|
||||||
.set(latitude, longitude)
|
|
||||||
.await
|
|
||||||
.expect("Failed to set location");
|
|
||||||
|
|
||||||
println!("Location set!");
|
|
||||||
println!("Press ctrl-c to stop");
|
|
||||||
loop {
|
|
||||||
ls_client
|
ls_client
|
||||||
.set(latitude, longitude)
|
.set(latitude, longitude)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to set location");
|
.expect("Failed to set location");
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
|
||||||
|
println!("Location set!");
|
||||||
|
println!("Press ctrl-c to stop");
|
||||||
|
loop {
|
||||||
|
ls_client
|
||||||
|
.set(latitude, longitude)
|
||||||
|
.await
|
||||||
|
.expect("Failed to set location");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Invalid usage, pass -h for help");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
let mut location_client = match LocationSimulationService::connect(&*provider).await {
|
||||||
}
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Unable to connect to simulate_location service: {e} Ensure Developer Disk Image is mounted."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if matches.subcommand_matches("clear").is_some() {
|
||||||
|
location_client.clear().await.expect("Unable to clear");
|
||||||
|
println!("Location cleared!");
|
||||||
|
} else if let Some(matches) = matches.subcommand_matches("set") {
|
||||||
|
let latitude: &String = match matches.get_one("latitude") {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
eprintln!("No latitude passed! Pass -h for help");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let longitude: &String = match matches.get_one("longitude") {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
eprintln!("No longitude passed! Pass -h for help");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
location_client
|
||||||
|
.set(latitude, longitude)
|
||||||
|
.await
|
||||||
|
.expect("Failed to set location");
|
||||||
|
|
||||||
|
println!("Location set!");
|
||||||
|
} else {
|
||||||
|
eprintln!("Invalid usage, pass -h for help");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
101
tools/src/notifications.rs
Normal file
101
tools/src/notifications.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Monitor memory and app notifications
|
||||||
|
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
let matches = Command::new("notifications")
|
||||||
|
.about("start notifications")
|
||||||
|
.arg(
|
||||||
|
Arg::new("host")
|
||||||
|
.long("host")
|
||||||
|
.value_name("HOST")
|
||||||
|
.help("IP address of the device"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("pairing_file")
|
||||||
|
.long("pairing-file")
|
||||||
|
.value_name("PATH")
|
||||||
|
.help("Path to the pairing file"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("udid")
|
||||||
|
.value_name("UDID")
|
||||||
|
.help("UDID of the device (overrides host/pairing file)")
|
||||||
|
.index(1),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("about")
|
||||||
|
.long("about")
|
||||||
|
.help("Show about information")
|
||||||
|
.action(clap::ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
if matches.get_flag("about") {
|
||||||
|
print!("notifications - start notifications to ios device");
|
||||||
|
println!("Copyright (c) 2025 Jackson Coxson");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let udid = matches.get_one::<String>("udid");
|
||||||
|
let host = matches.get_one::<String>("host");
|
||||||
|
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||||
|
|
||||||
|
let provider =
|
||||||
|
match common::get_provider(udid, host, pairing_file, "notifications-jkcoxson").await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
|
.await
|
||||||
|
.expect("no core proxy");
|
||||||
|
let rsd_port = proxy.handshake.server_rsd_port;
|
||||||
|
|
||||||
|
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||||
|
|
||||||
|
let mut adapter = adapter.to_async_handle();
|
||||||
|
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
|
||||||
|
|
||||||
|
// Make the connection to RemoteXPC
|
||||||
|
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
||||||
|
let mut ts_client =
|
||||||
|
idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect");
|
||||||
|
ts_client.read_message(0).await.expect("no read??");
|
||||||
|
let mut notification_client =
|
||||||
|
idevice::dvt::notifications::NotificationsClient::new(&mut ts_client)
|
||||||
|
.await
|
||||||
|
.expect("Unable to get channel for notifications");
|
||||||
|
notification_client
|
||||||
|
.start_notifications()
|
||||||
|
.await
|
||||||
|
.expect("Failed to start notifications");
|
||||||
|
|
||||||
|
// Handle Ctrl+C gracefully
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
println!("\nShutdown signal received, exiting.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch 2: Wait for the next batch of notifications.
|
||||||
|
result = notification_client.get_notification() => {
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("Failed to get notifications: {}", e);
|
||||||
|
} else {
|
||||||
|
println!("Received notifications: {:#?}", result.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ use idevice::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
mod pcap;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|||||||
109
tools/src/screenshot.rs
Normal file
109
tools/src/screenshot.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use clap::{Arg, Command};
|
||||||
|
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use idevice::screenshotr::ScreenshotService;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
let matches = Command::new("screen_shot")
|
||||||
|
.about("take screenshot")
|
||||||
|
.arg(
|
||||||
|
Arg::new("host")
|
||||||
|
.long("host")
|
||||||
|
.value_name("HOST")
|
||||||
|
.help("IP address of the device"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("pairing_file")
|
||||||
|
.long("pairing-file")
|
||||||
|
.value_name("PATH")
|
||||||
|
.help("Path to the pairing file"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("udid")
|
||||||
|
.value_name("UDID")
|
||||||
|
.help("UDID of the device (overrides host/pairing file)")
|
||||||
|
.index(1),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("output")
|
||||||
|
.short('o')
|
||||||
|
.long("output")
|
||||||
|
.value_name("FILE")
|
||||||
|
.help("Output file path for the screenshot (default: ./screenshot.png)")
|
||||||
|
.default_value("screenshot.png"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("about")
|
||||||
|
.long("about")
|
||||||
|
.help("Show about information")
|
||||||
|
.action(clap::ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
if matches.get_flag("about") {
|
||||||
|
print!("screen_shot - take screenshot from ios device");
|
||||||
|
println!("Copyright (c) 2025 Jackson Coxson");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let udid = matches.get_one::<String>("udid");
|
||||||
|
let host = matches.get_one::<String>("host");
|
||||||
|
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||||
|
let output_path = matches.get_one::<String>("output").unwrap();
|
||||||
|
|
||||||
|
let provider =
|
||||||
|
match common::get_provider(udid, host, pairing_file, "take_screenshot-jkcoxson").await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
|
||||||
|
let rsd_port = proxy.handshake.server_rsd_port;
|
||||||
|
|
||||||
|
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
|
||||||
|
let mut adapter = adapter.to_async_handle();
|
||||||
|
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
|
||||||
|
|
||||||
|
// Make the connection to RemoteXPC
|
||||||
|
let mut handshake = RsdHandshake::new(stream).await.unwrap();
|
||||||
|
let mut ts_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd(
|
||||||
|
&mut adapter,
|
||||||
|
&mut handshake,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect");
|
||||||
|
ts_client.read_message(0).await.expect("no read??");
|
||||||
|
|
||||||
|
let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client)
|
||||||
|
.await
|
||||||
|
.expect("Unable to get channel for take screenshot");
|
||||||
|
ts_client
|
||||||
|
.take_screenshot()
|
||||||
|
.await
|
||||||
|
.expect("Failed to take screenshot")
|
||||||
|
} else {
|
||||||
|
let mut screenshot_client = match ScreenshotService::connect(&*provider).await {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Unable to connect to screenshotr service: {e} Ensure Developer Disk Image is mounted."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screenshot_client.take_screenshot().await.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
match fs::write(output_path, res) {
|
||||||
|
Ok(_) => println!("Screenshot saved to: {}", output_path),
|
||||||
|
Err(e) => eprintln!("Failed to write screenshot to file: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user