mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Merge master into rppairing-try2
This commit is contained in:
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -30,8 +30,14 @@ 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-ios-sim && \
|
||||||
rustup target add aarch64-apple-darwin && \
|
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 && \
|
||||||
|
rustup target add aarch64-apple-ios-macabi && \
|
||||||
|
rustup target add x86_64-apple-ios-macabi && \
|
||||||
|
cargo install --force --locked bindgen-cli
|
||||||
|
|
||||||
- name: Build all Apple targets and examples/tools
|
- name: Build all Apple targets and examples/tools
|
||||||
run: |
|
run: |
|
||||||
@@ -44,6 +50,12 @@ 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:
|
||||||
|
|||||||
3870
Cargo.lock
generated
3870
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
README.md
25
README.md
@@ -47,6 +47,8 @@ To keep dependency bloat and compile time down, everything is contained in featu
|
|||||||
|------------------------|-----------------------------------------------------------------------------|
|
|------------------------|-----------------------------------------------------------------------------|
|
||||||
| `afc` | Apple File Conduit for file system access.|
|
| `afc` | Apple File Conduit for file system access.|
|
||||||
| `amfi` | Apple mobile file integrity service |
|
| `amfi` | Apple mobile file integrity service |
|
||||||
|
| `bt_packet_logger` | Capture Bluetooth packets. |
|
||||||
|
| `companion_proxy` | Manage paired Apple Watches. |
|
||||||
| `core_device_proxy` | Start a secure tunnel to access protected services. |
|
| `core_device_proxy` | Start a secure tunnel to access protected services. |
|
||||||
| `crashreportcopymobile`| Copy crash reports.|
|
| `crashreportcopymobile`| Copy crash reports.|
|
||||||
| `debug_proxy` | Send GDB commands to the device.|
|
| `debug_proxy` | Send GDB commands to the device.|
|
||||||
@@ -55,13 +57,19 @@ To keep dependency bloat and compile time down, everything is contained in featu
|
|||||||
| `heartbeat` | Maintain a heartbeat connection.|
|
| `heartbeat` | Maintain a heartbeat connection.|
|
||||||
| `house_arrest` | Manage files in app containers |
|
| `house_arrest` | Manage files in app containers |
|
||||||
| `installation_proxy` | Manage app installation and uninstallation.|
|
| `installation_proxy` | Manage app installation and uninstallation.|
|
||||||
| `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.|
|
| `installcoordination_proxy` | Manage app installation coordination.|
|
||||||
| `misagent` | Manage provisioning profiles on the device.|
|
|
||||||
| `mobilebackup2` | Manage backups.|
|
|
||||||
| `mobile_image_mounter` | Manage DDI images.|
|
|
||||||
| `location_simulation` | Simulate GPS locations on the device.|
|
| `location_simulation` | Simulate GPS locations on the device.|
|
||||||
|
| `misagent` | Manage provisioning profiles on the device.|
|
||||||
|
| `mobile_image_mounter` | Manage DDI images.|
|
||||||
|
| `mobileactivationd` | Activate/Deactivate device.|
|
||||||
|
| `mobilebackup2` | Manage backups.|
|
||||||
| `pair` | Pair the device.|
|
| `pair` | Pair the device.|
|
||||||
| `syslog_relay` | Relay system logs from the device |
|
| `pcapd` | Capture network packets.|
|
||||||
|
| `preboard_service` | Interface with Preboard.|
|
||||||
|
| `restore_service` | Restore service (recovery/reboot).|
|
||||||
|
| `screenshotr` | Take screenshots.|
|
||||||
|
| `springboardservices` | Control SpringBoard (icons, wallpaper, orientation, etc.).|
|
||||||
|
| `syslog_relay` | Relay system logs and OS trace logs from the device. |
|
||||||
| `tcp` | Connect to devices over TCP.|
|
| `tcp` | Connect to devices over TCP.|
|
||||||
| `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.|
|
| `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.|
|
||||||
| `tss` | Make requests to Apple's TSS servers. Partial support.|
|
| `tss` | Make requests to Apple's TSS servers. Partial support.|
|
||||||
@@ -73,16 +81,11 @@ To keep dependency bloat and compile time down, everything is contained in featu
|
|||||||
|
|
||||||
Finish the following:
|
Finish the following:
|
||||||
|
|
||||||
- springboard
|
- webinspector
|
||||||
|
|
||||||
Implement the following:
|
Implement the following:
|
||||||
|
|
||||||
- companion_proxy
|
|
||||||
- diagnostics
|
|
||||||
- mobilebackup2
|
|
||||||
- notification_proxy
|
- notification_proxy
|
||||||
- screenshot
|
|
||||||
- webinspector
|
|
||||||
|
|
||||||
As this project is done in my free time within my busy schedule, there
|
As this project is done in my free time within my busy schedule, there
|
||||||
is no ETA for any of these. Feel free to contribute or donate!
|
is no ETA for any of these. Feel free to contribute or donate!
|
||||||
|
|||||||
60
cpp/include/idevice++/diagnostics_relay.hpp
Normal file
60
cpp/include/idevice++/diagnostics_relay.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <idevice++/result.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using DiagnosticsRelayPtr =
|
||||||
|
std::unique_ptr<DiagnosticsRelayClientHandle,
|
||||||
|
FnDeleter<DiagnosticsRelayClientHandle, diagnostics_relay_client_free>>;
|
||||||
|
|
||||||
|
class DiagnosticsRelay {
|
||||||
|
public:
|
||||||
|
// Factory: connect via Provider
|
||||||
|
static Result<DiagnosticsRelay, FfiError> connect(Provider& provider);
|
||||||
|
|
||||||
|
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||||
|
static Result<DiagnosticsRelay, FfiError> from_socket(Idevice&& socket);
|
||||||
|
|
||||||
|
// API Methods - queries returning optional plist
|
||||||
|
Result<Option<plist_t>, FfiError> ioregistry(Option<std::string> current_plane,
|
||||||
|
Option<std::string> entry_name,
|
||||||
|
Option<std::string> entry_class) const;
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> mobilegestalt(Option<std::vector<char*>> keys) const;
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> gasguage() const;
|
||||||
|
Result<Option<plist_t>, FfiError> nand() const;
|
||||||
|
Result<Option<plist_t>, FfiError> all() const;
|
||||||
|
Result<Option<plist_t>, FfiError> wifi() const;
|
||||||
|
|
||||||
|
// API Methods - actions
|
||||||
|
Result<void, FfiError> restart();
|
||||||
|
Result<void, FfiError> shutdown();
|
||||||
|
Result<void, FfiError> sleep();
|
||||||
|
Result<void, FfiError> goodbye();
|
||||||
|
|
||||||
|
// RAII / moves
|
||||||
|
~DiagnosticsRelay() noexcept = default;
|
||||||
|
DiagnosticsRelay(DiagnosticsRelay&&) noexcept = default;
|
||||||
|
DiagnosticsRelay& operator=(DiagnosticsRelay&&) noexcept = default;
|
||||||
|
DiagnosticsRelay(const DiagnosticsRelay&) = delete;
|
||||||
|
DiagnosticsRelay& operator=(const DiagnosticsRelay&) = delete;
|
||||||
|
|
||||||
|
DiagnosticsRelayClientHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static DiagnosticsRelay adopt(DiagnosticsRelayClientHandle* h) noexcept {
|
||||||
|
return DiagnosticsRelay(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit DiagnosticsRelay(DiagnosticsRelayClientHandle* h) noexcept : handle_(h) {}
|
||||||
|
DiagnosticsRelayPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
46
cpp/include/idevice++/notification_proxy.hpp
Normal file
46
cpp/include/idevice++/notification_proxy.hpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
using NotificationProxyPtr = std::unique_ptr<NotificationProxyClientHandle,
|
||||||
|
FnDeleter<NotificationProxyClientHandle, notification_proxy_client_free>>;
|
||||||
|
|
||||||
|
class NotificationProxy {
|
||||||
|
public:
|
||||||
|
// Factory: connect via Provider
|
||||||
|
static Result<NotificationProxy, FfiError> connect(Provider& provider);
|
||||||
|
|
||||||
|
// Factory: wrap an existing Idevice socket (consumes it on success)
|
||||||
|
static Result<NotificationProxy, FfiError> from_socket(Idevice&& socket);
|
||||||
|
|
||||||
|
// Ops
|
||||||
|
Result<void, FfiError> post_notification(const std::string& name);
|
||||||
|
Result<void, FfiError> observe_notification(const std::string& name);
|
||||||
|
Result<void, FfiError> observe_notifications(const std::vector<std::string>& names);
|
||||||
|
Result<std::string, FfiError> receive_notification();
|
||||||
|
Result<std::string, FfiError> receive_notification_with_timeout(u_int64_t interval);
|
||||||
|
|
||||||
|
// RAII / moves
|
||||||
|
~NotificationProxy() noexcept = default;
|
||||||
|
NotificationProxy(NotificationProxy&&) noexcept = default;
|
||||||
|
NotificationProxy& operator=(NotificationProxy&&) noexcept = default;
|
||||||
|
NotificationProxy(const NotificationProxy&) = delete;
|
||||||
|
NotificationProxy& operator=(const NotificationProxy&) = delete;
|
||||||
|
|
||||||
|
NotificationProxyClientHandle* raw() const noexcept { return handle_.get(); }
|
||||||
|
static NotificationProxy adopt(NotificationProxyClientHandle* h) noexcept {
|
||||||
|
return NotificationProxy(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit NotificationProxy(NotificationProxyClientHandle* h) noexcept : handle_(h) {}
|
||||||
|
NotificationProxyPtr handle_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
159
cpp/src/diagnostics_relay.cpp
Normal file
159
cpp/src/diagnostics_relay.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/diagnostics_relay.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
// -------- Factory Methods --------
|
||||||
|
|
||||||
|
Result<DiagnosticsRelay, FfiError> DiagnosticsRelay::connect(Provider& provider) {
|
||||||
|
DiagnosticsRelayClientHandle* out = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_connect(provider.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(DiagnosticsRelay::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<DiagnosticsRelay, FfiError> DiagnosticsRelay::from_socket(Idevice&& socket) {
|
||||||
|
DiagnosticsRelayClientHandle* out = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_new(socket.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
socket.release();
|
||||||
|
return Ok(DiagnosticsRelay::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- API Methods --------
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError>
|
||||||
|
DiagnosticsRelay::ioregistry(Option<std::string> current_plane,
|
||||||
|
Option<std::string> entry_name,
|
||||||
|
Option<std::string> entry_class) const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
|
||||||
|
const char* plane_ptr = current_plane.is_some() ? current_plane.unwrap().c_str() : nullptr;
|
||||||
|
const char* name_ptr = entry_name.is_some() ? entry_name.unwrap().c_str() : nullptr;
|
||||||
|
const char* class_ptr = entry_class.is_some() ? entry_class.unwrap().c_str() : nullptr;
|
||||||
|
|
||||||
|
FfiError e(
|
||||||
|
::diagnostics_relay_client_ioregistry(handle_.get(), plane_ptr, name_ptr, class_ptr, &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError>
|
||||||
|
DiagnosticsRelay::mobilegestalt(Option<std::vector<char*>> keys) const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
|
||||||
|
if (!keys.is_some() || keys.unwrap().empty()) {
|
||||||
|
return Err(FfiError::InvalidArgument());
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiError e(::diagnostics_relay_client_mobilegestalt(
|
||||||
|
handle_.get(), keys.unwrap().data(), keys.unwrap().size(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::gasguage() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_gasguage(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::nand() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_nand(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::all() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_all(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Option<plist_t>, FfiError> DiagnosticsRelay::wifi() const {
|
||||||
|
plist_t res = nullptr;
|
||||||
|
FfiError e(::diagnostics_relay_client_wifi(handle_.get(), &res));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == nullptr) {
|
||||||
|
return Ok(Option<plist_t>(None));
|
||||||
|
}
|
||||||
|
return Ok(Some(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::restart() {
|
||||||
|
FfiError e(::diagnostics_relay_client_restart(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::shutdown() {
|
||||||
|
FfiError e(::diagnostics_relay_client_shutdown(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::sleep() {
|
||||||
|
FfiError e(::diagnostics_relay_client_sleep(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> DiagnosticsRelay::goodbye() {
|
||||||
|
FfiError e(::diagnostics_relay_client_goodbye(handle_.get()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
82
cpp/src/notification_proxy.cpp
Normal file
82
cpp/src/notification_proxy.cpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#include <idevice++/bindings.hpp>
|
||||||
|
#include <idevice++/ffi.hpp>
|
||||||
|
#include <idevice++/notification_proxy.hpp>
|
||||||
|
#include <idevice++/provider.hpp>
|
||||||
|
|
||||||
|
namespace IdeviceFFI {
|
||||||
|
|
||||||
|
Result<NotificationProxy, FfiError> NotificationProxy::connect(Provider& provider) {
|
||||||
|
NotificationProxyClientHandle* out = nullptr;
|
||||||
|
FfiError e(::notification_proxy_connect(provider.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
provider.release();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(NotificationProxy::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<NotificationProxy, FfiError> NotificationProxy::from_socket(Idevice&& socket) {
|
||||||
|
NotificationProxyClientHandle* out = nullptr;
|
||||||
|
FfiError e(::notification_proxy_new(socket.raw(), &out));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
socket.release();
|
||||||
|
return Ok(NotificationProxy::adopt(out));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> NotificationProxy::post_notification(const std::string& name) {
|
||||||
|
FfiError e(::notification_proxy_post(handle_.get(), name.c_str()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> NotificationProxy::observe_notification(const std::string& name) {
|
||||||
|
FfiError e(::notification_proxy_observe(handle_.get(), name.c_str()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void, FfiError> NotificationProxy::observe_notifications(const std::vector<std::string>& names) {
|
||||||
|
std::vector<const char*> ptrs;
|
||||||
|
ptrs.reserve(names.size() + 1);
|
||||||
|
for (const auto& n : names) {
|
||||||
|
ptrs.push_back(n.c_str());
|
||||||
|
}
|
||||||
|
ptrs.push_back(nullptr);
|
||||||
|
FfiError e(::notification_proxy_observe_multiple(handle_.get(), ptrs.data()));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::string, FfiError> NotificationProxy::receive_notification() {
|
||||||
|
char* name_ptr = nullptr;
|
||||||
|
FfiError e(::notification_proxy_receive(handle_.get(), &name_ptr));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
std::string name(name_ptr);
|
||||||
|
::notification_proxy_free_string(name_ptr);
|
||||||
|
return Ok(std::move(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::string, FfiError> NotificationProxy::receive_notification_with_timeout(u_int64_t interval) {
|
||||||
|
char* name_ptr = nullptr;
|
||||||
|
FfiError e(::notification_proxy_receive_with_timeout(handle_.get(), interval, &name_ptr));
|
||||||
|
if (e) {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
std::string name(name_ptr);
|
||||||
|
::notification_proxy_free_string(name_ptr);
|
||||||
|
return Ok(std::move(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace IdeviceFFI
|
||||||
@@ -34,6 +34,7 @@ debug_proxy = ["idevice/debug_proxy"]
|
|||||||
diagnostics_relay = ["idevice/diagnostics_relay"]
|
diagnostics_relay = ["idevice/diagnostics_relay"]
|
||||||
dvt = ["idevice/dvt"]
|
dvt = ["idevice/dvt"]
|
||||||
heartbeat = ["idevice/heartbeat"]
|
heartbeat = ["idevice/heartbeat"]
|
||||||
|
notification_proxy = ["idevice/notification_proxy"]
|
||||||
house_arrest = ["idevice/house_arrest"]
|
house_arrest = ["idevice/house_arrest"]
|
||||||
installation_proxy = ["idevice/installation_proxy"]
|
installation_proxy = ["idevice/installation_proxy"]
|
||||||
springboardservices = ["idevice/springboardservices"]
|
springboardservices = ["idevice/springboardservices"]
|
||||||
@@ -50,6 +51,7 @@ tss = ["idevice/tss"]
|
|||||||
tunneld = ["idevice/tunneld"]
|
tunneld = ["idevice/tunneld"]
|
||||||
usbmuxd = ["idevice/usbmuxd"]
|
usbmuxd = ["idevice/usbmuxd"]
|
||||||
xpc = ["idevice/xpc"]
|
xpc = ["idevice/xpc"]
|
||||||
|
screenshotr = ["idevice/screenshotr"]
|
||||||
full = [
|
full = [
|
||||||
"afc",
|
"afc",
|
||||||
"amfi",
|
"amfi",
|
||||||
@@ -60,6 +62,7 @@ full = [
|
|||||||
"diagnostics_relay",
|
"diagnostics_relay",
|
||||||
"dvt",
|
"dvt",
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
|
"notification_proxy",
|
||||||
"house_arrest",
|
"house_arrest",
|
||||||
"installation_proxy",
|
"installation_proxy",
|
||||||
"misagent",
|
"misagent",
|
||||||
@@ -75,6 +78,7 @@ full = [
|
|||||||
"tunneld",
|
"tunneld",
|
||||||
"springboardservices",
|
"springboardservices",
|
||||||
"syslog_relay",
|
"syslog_relay",
|
||||||
|
"screenshotr",
|
||||||
]
|
]
|
||||||
default = ["full", "aws-lc"]
|
default = ["full", "aws-lc"]
|
||||||
|
|
||||||
|
|||||||
12
ffi/build.rs
12
ffi/build.rs
@@ -35,14 +35,20 @@ fn main() {
|
|||||||
.expect("Unable to generate bindings")
|
.expect("Unable to generate bindings")
|
||||||
.write_to_file("idevice.h");
|
.write_to_file("idevice.h");
|
||||||
|
|
||||||
|
// Check if plist.h exists locally first, otherwise download
|
||||||
|
let plist_h_path = "plist.h";
|
||||||
|
let h = if std::path::Path::new(plist_h_path).exists() {
|
||||||
|
std::fs::read_to_string(plist_h_path).expect("failed to read plist.h")
|
||||||
|
} else {
|
||||||
// download plist.h
|
// download plist.h
|
||||||
let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h")
|
let h = ureq::get("https://raw.githubusercontent.com/libimobiledevice/libplist/refs/heads/master/include/plist/plist.h")
|
||||||
.call()
|
.call()
|
||||||
.expect("failed to download plist.h");
|
.expect("failed to download plist.h");
|
||||||
let h = h
|
h.into_body()
|
||||||
.into_body()
|
|
||||||
.read_to_string()
|
.read_to_string()
|
||||||
.expect("failed to get string content");
|
.expect("failed to get string content")
|
||||||
|
};
|
||||||
|
|
||||||
let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap();
|
let mut f = OpenOptions::new().append(true).open("idevice.h").unwrap();
|
||||||
f.write_all(b"\n\n\n").unwrap();
|
f.write_all(b"\n\n\n").unwrap();
|
||||||
f.write_all(&h.into_bytes())
|
f.write_all(&h.into_bytes())
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ int main(int argc, char **argv) {
|
|||||||
} else {
|
} else {
|
||||||
uint8_t *data = NULL;
|
uint8_t *data = NULL;
|
||||||
size_t length = 0;
|
size_t length = 0;
|
||||||
err = afc_file_read(file, &data, &length);
|
err = afc_file_read_entire(file, &data, &length);
|
||||||
if (err == NULL) {
|
if (err == NULL) {
|
||||||
if (write_file(dest_path, data, length)) {
|
if (write_file(dest_path, data, length)) {
|
||||||
printf("File downloaded successfully\n");
|
printf("File downloaded successfully\n");
|
||||||
|
|||||||
189
ffi/src/afc.rs
189
ffi/src/afc.rs
@@ -1,12 +1,13 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use std::ptr::null_mut;
|
use std::{io::SeekFrom, ptr::null_mut};
|
||||||
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceError, IdeviceService,
|
IdeviceError, IdeviceService,
|
||||||
afc::{AfcClient, DeviceInfo, FileInfo},
|
afc::{AfcClient, DeviceInfo, FileInfo, file::FileDescriptor},
|
||||||
provider::IdeviceProvider,
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
IdeviceFfiError, IdeviceHandle, LOCAL_RUNTIME, ffi_err, provider::IdeviceProviderHandle,
|
IdeviceFfiError, IdeviceHandle, LOCAL_RUNTIME, ffi_err, provider::IdeviceProviderHandle,
|
||||||
@@ -53,6 +54,44 @@ pub unsafe extern "C" fn afc_client_connect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connects to the AFC2 service using a TCP provider
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`provider`] - An IdeviceProvider
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `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
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn afc2_client_connect(
|
||||||
|
provider: *mut IdeviceProviderHandle,
|
||||||
|
client: *mut *mut AfcClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if provider.is_null() || client.is_null() {
|
||||||
|
tracing::error!("Null pointer provided");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = run_sync_local(async {
|
||||||
|
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||||
|
|
||||||
|
AfcClient::new_afc2(provider_ref).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let boxed = Box::new(AfcClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new AfcClient from an existing Idevice connection
|
/// Creates a new AfcClient from an existing Idevice connection
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -555,12 +594,13 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads data from an open file
|
/// Reads data from an open file. This advances the cursor of the file.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * [`handle`] - File handle to read from
|
/// * [`handle`] - File handle to read from
|
||||||
/// * [`data`] - Will be set to point to the read data
|
/// * [`data`] - Will be set to point to the read data
|
||||||
/// * [`length`] - Will be set to the length of the read data
|
/// * [`len`] - Number of bytes to read from the file
|
||||||
|
/// * [`bytes_read`] - The number of bytes read from the file
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// An IdeviceFfiError on error, null on success
|
/// An IdeviceFfiError on error, null on success
|
||||||
@@ -569,6 +609,53 @@ pub unsafe extern "C" fn afc_file_close(handle: *mut AfcFileHandle) -> *mut Idev
|
|||||||
/// All pointers must be valid and non-null
|
/// All pointers must be valid and non-null
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn afc_file_read(
|
pub unsafe extern "C" fn afc_file_read(
|
||||||
|
handle: *mut AfcFileHandle,
|
||||||
|
data: *mut *mut u8,
|
||||||
|
len: usize,
|
||||||
|
bytes_read: *mut libc::size_t,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if handle.is_null() || data.is_null() || bytes_read.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
|
||||||
|
let res: Result<Vec<u8>, IdeviceError> = run_sync({
|
||||||
|
let mut buf = vec![0u8; len];
|
||||||
|
async move {
|
||||||
|
let r = fd.read(&mut buf).await?;
|
||||||
|
buf.truncate(r);
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(bytes) => {
|
||||||
|
let mut boxed = bytes.into_boxed_slice();
|
||||||
|
unsafe {
|
||||||
|
*data = boxed.as_mut_ptr();
|
||||||
|
*bytes_read = boxed.len();
|
||||||
|
}
|
||||||
|
std::mem::forget(boxed);
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads all data from an open file.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - File handle to read from
|
||||||
|
/// * [`data`] - Will be set to point to the read data
|
||||||
|
/// * [`length`] - The number of bytes read from the file
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// All pointers must be valid and non-null
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn afc_file_read_entire(
|
||||||
handle: *mut AfcFileHandle,
|
handle: *mut AfcFileHandle,
|
||||||
data: *mut *mut u8,
|
data: *mut *mut u8,
|
||||||
length: *mut libc::size_t,
|
length: *mut libc::size_t,
|
||||||
@@ -594,6 +681,100 @@ pub unsafe extern "C" fn afc_file_read(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the read/write cursor in an open file.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - File handle whose cursor should be moved
|
||||||
|
/// * [`offset`] - Distance to move the cursor, interpreted based on `whence`
|
||||||
|
/// * [`whence`] - Origin used for the seek operation:
|
||||||
|
/// * `0` — Seek from the start of the file (`SeekFrom::Start`)
|
||||||
|
/// * `1` — Seek from the current cursor position (`SeekFrom::Current`)
|
||||||
|
/// * `2` — Seek from the end of the file (`SeekFrom::End`)
|
||||||
|
/// * [`new_pos`] - Output parameter; will be set to the new absolute cursor position
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An [`IdeviceFfiError`] on error, or null on success.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// All pointers must be valid and non-null.
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
/// * If `whence` is invalid, this function returns `FfiInvalidArg`.
|
||||||
|
/// * The AFC protocol may restrict seeking beyond certain bounds; such errors
|
||||||
|
/// are reported through the returned [`IdeviceFfiError`].
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn afc_file_seek(
|
||||||
|
handle: *mut AfcFileHandle,
|
||||||
|
offset: i64,
|
||||||
|
whence: libc::c_int,
|
||||||
|
new_pos: *mut i64,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if handle.is_null() || new_pos.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
|
||||||
|
|
||||||
|
let seek_from = match whence {
|
||||||
|
0 => SeekFrom::Start(offset as u64),
|
||||||
|
1 => SeekFrom::Current(offset),
|
||||||
|
2 => SeekFrom::End(offset),
|
||||||
|
_ => return ffi_err!(IdeviceError::FfiInvalidArg),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res: Result<u64, IdeviceError> = run_sync(async move { Ok(fd.seek(seek_from).await?) });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(pos) => {
|
||||||
|
unsafe {
|
||||||
|
*new_pos = pos as i64;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current read/write cursor position of an open file.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - File handle whose cursor should be queried
|
||||||
|
/// * [`pos`] - Output parameter; will be set to the current absolute cursor position
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An [`IdeviceFfiError`] on error, or null on success.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// All pointers must be valid and non-null.
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
/// This function is equivalent to performing a seek operation with
|
||||||
|
/// `SeekFrom::Current(0)` internally.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn afc_file_tell(
|
||||||
|
handle: *mut AfcFileHandle,
|
||||||
|
pos: *mut i64,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if handle.is_null() || pos.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fd = unsafe { &mut *(handle as *mut FileDescriptor) };
|
||||||
|
|
||||||
|
let res: Result<u64, IdeviceError> =
|
||||||
|
run_sync(async { Ok(fd.seek(SeekFrom::Current(0)).await?) });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(cur) => {
|
||||||
|
unsafe {
|
||||||
|
*pos = cur as i64;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Writes data to an open file
|
/// Writes data to an open file
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ pub unsafe extern "C" fn diagnostics_relay_client_mobilegestalt(
|
|||||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keys = if keys.is_null() {
|
let keys = if !keys.is_null() {
|
||||||
let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) };
|
let keys = unsafe { std::slice::from_raw_parts(keys, keys_len) };
|
||||||
Some(
|
Some(
|
||||||
keys.iter()
|
keys.iter()
|
||||||
|
|||||||
183
ffi/src/house_arrest.rs
Normal file
183
ffi/src/house_arrest.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
ffi::{CStr, c_char},
|
||||||
|
ptr::null_mut,
|
||||||
|
};
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceError, IdeviceService, afc::AfcClient, house_arrest::HouseArrestClient,
|
||||||
|
provider::IdeviceProvider,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle,
|
||||||
|
run_sync_local,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HouseArrestClientHandle(pub HouseArrestClient);
|
||||||
|
|
||||||
|
/// Connects to the House Arrest service using a TCP provider
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`provider`] - An IdeviceProvider
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `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
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn house_arrest_client_connect(
|
||||||
|
provider: *mut IdeviceProviderHandle,
|
||||||
|
client: *mut *mut HouseArrestClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if provider.is_null() || client.is_null() {
|
||||||
|
tracing::error!("Null pointer provided");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = run_sync_local(async {
|
||||||
|
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||||
|
|
||||||
|
HouseArrestClient::connect(provider_ref).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let boxed = Box::new(HouseArrestClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new HouseArrestClient from an existing Idevice connection
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`socket`] - An IdeviceSocket handle
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated HouseArrestClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `socket` 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
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn house_arrest_client_new(
|
||||||
|
socket: *mut IdeviceHandle,
|
||||||
|
client: *mut *mut HouseArrestClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if socket.is_null() || client.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
let socket = unsafe { Box::from_raw(socket) }.0;
|
||||||
|
let r = HouseArrestClient::new(socket);
|
||||||
|
let boxed = Box::new(HouseArrestClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vends a container for an app
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`client`] - The House Arrest client
|
||||||
|
/// * [`bundle_id`] - The bundle ID to vend for
|
||||||
|
/// * [`afc_client`] - The new AFC client for the underlying connection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a allocated by this library
|
||||||
|
/// `bundle_id` must be a NULL-terminated string
|
||||||
|
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn house_arrest_vend_container(
|
||||||
|
client: *mut HouseArrestClientHandle,
|
||||||
|
bundle_id: *const c_char,
|
||||||
|
afc_client: *mut *mut AfcClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || bundle_id.is_null() || afc_client.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy();
|
||||||
|
let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop
|
||||||
|
|
||||||
|
let res: Result<AfcClient, IdeviceError> =
|
||||||
|
run_sync_local(async move { client_ref.vend_container(bundle_id).await });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(a) => {
|
||||||
|
let a = Box::into_raw(Box::new(AfcClientHandle(a)));
|
||||||
|
unsafe { *afc_client = a };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ffi_err!(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vends documents for an app
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`client`] - The House Arrest client
|
||||||
|
/// * [`bundle_id`] - The bundle ID to vend for
|
||||||
|
/// * [`afc_client`] - The new AFC client for the underlying connection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a allocated by this library
|
||||||
|
/// `bundle_id` must be a NULL-terminated string
|
||||||
|
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn house_arrest_vend_documents(
|
||||||
|
client: *mut HouseArrestClientHandle,
|
||||||
|
bundle_id: *const c_char,
|
||||||
|
afc_client: *mut *mut AfcClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || bundle_id.is_null() || afc_client.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bundle_id = unsafe { CStr::from_ptr(bundle_id) }.to_string_lossy();
|
||||||
|
let client_ref = unsafe { Box::from_raw(client) }.0; // take ownership and drop
|
||||||
|
|
||||||
|
let res: Result<AfcClient, IdeviceError> =
|
||||||
|
run_sync_local(async move { client_ref.vend_documents(bundle_id).await });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(a) => {
|
||||||
|
let a = Box::into_raw(Box::new(AfcClientHandle(a)));
|
||||||
|
unsafe { *afc_client = a };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ffi_err!(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees an HouseArrestClient handle
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - The handle to free
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `handle` must be a valid pointer to the handle that was allocated by this library,
|
||||||
|
/// or NULL (in which case this function does nothing)
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn house_arrest_client_free(handle: *mut HouseArrestClientHandle) {
|
||||||
|
if !handle.is_null() {
|
||||||
|
tracing::debug!("Freeing house_arrest_client");
|
||||||
|
let _ = unsafe { Box::from_raw(handle) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ pub mod dvt;
|
|||||||
mod errors;
|
mod errors;
|
||||||
#[cfg(feature = "heartbeat")]
|
#[cfg(feature = "heartbeat")]
|
||||||
pub mod heartbeat;
|
pub mod heartbeat;
|
||||||
|
#[cfg(feature = "house_arrest")]
|
||||||
|
pub mod house_arrest;
|
||||||
#[cfg(feature = "installation_proxy")]
|
#[cfg(feature = "installation_proxy")]
|
||||||
pub mod installation_proxy;
|
pub mod installation_proxy;
|
||||||
pub mod lockdown;
|
pub mod lockdown;
|
||||||
@@ -27,12 +29,16 @@ pub mod logging;
|
|||||||
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 = "notification_proxy")]
|
||||||
|
pub mod notification_proxy;
|
||||||
#[cfg(feature = "syslog_relay")]
|
#[cfg(feature = "syslog_relay")]
|
||||||
pub mod os_trace_relay;
|
pub mod os_trace_relay;
|
||||||
mod pairing_file;
|
mod pairing_file;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
#[cfg(feature = "xpc")]
|
#[cfg(feature = "xpc")]
|
||||||
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")]
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ pub unsafe extern "C" fn lockdownd_get_value(
|
|||||||
domain: *const libc::c_char,
|
domain: *const libc::c_char,
|
||||||
out_plist: *mut plist_t,
|
out_plist: *mut plist_t,
|
||||||
) -> *mut IdeviceFfiError {
|
) -> *mut IdeviceFfiError {
|
||||||
if out_plist.is_null() {
|
if client.is_null() || out_plist.is_null() {
|
||||||
return ffi_err!(IdeviceError::FfiInvalidArg);
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +221,89 @@ pub unsafe extern "C" fn lockdownd_get_value(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tells the device to enter recovery mode
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid LockdowndClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn lockdownd_enter_recovery(
|
||||||
|
client: *mut LockdowndClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.enter_recovery().await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => null_mut(),
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a value in lockdownd
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid LockdowndClient handle
|
||||||
|
/// * `key` - The key to set (null-terminated string)
|
||||||
|
/// * `value` - The value to set as a plist
|
||||||
|
/// * `domain` - The domain to set in (null-terminated string, optional)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `key` must be a valid null-terminated string
|
||||||
|
/// `value` must be a valid plist
|
||||||
|
/// `domain` must be a valid null-terminated string or NULL
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn lockdownd_set_value(
|
||||||
|
client: *mut LockdowndClientHandle,
|
||||||
|
key: *const libc::c_char,
|
||||||
|
value: plist_t,
|
||||||
|
domain: *const libc::c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || key.is_null() || value.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = match unsafe { std::ffi::CStr::from_ptr(key) }.to_str() {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::InvalidCString),
|
||||||
|
};
|
||||||
|
|
||||||
|
let domain = if domain.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(match unsafe { std::ffi::CStr::from_ptr(domain) }.to_str() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::InvalidCString),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = unsafe { &mut *value }.borrow_self().clone();
|
||||||
|
|
||||||
|
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.set_value(key, value, domain).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => null_mut(),
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Frees a LockdowndClient handle
|
/// Frees a LockdowndClient handle
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
311
ffi/src/notification_proxy.rs
Normal file
311
ffi/src/notification_proxy.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::ffi::{CStr, CString, c_char};
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceError, IdeviceService, notification_proxy::NotificationProxyClient,
|
||||||
|
provider::IdeviceProvider,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync_local,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct NotificationProxyClientHandle(pub NotificationProxyClient);
|
||||||
|
|
||||||
|
/// Automatically creates and connects to Notification Proxy, returning a client handle
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`provider`] - An IdeviceProvider
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `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
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_connect(
|
||||||
|
provider: *mut IdeviceProviderHandle,
|
||||||
|
client: *mut *mut NotificationProxyClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if provider.is_null() || client.is_null() {
|
||||||
|
tracing::error!("Null pointer provided");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<NotificationProxyClient, IdeviceError> = run_sync_local(async move {
|
||||||
|
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||||
|
NotificationProxyClient::connect(provider_ref).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let boxed = Box::new(NotificationProxyClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ffi_err!(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new NotificationProxyClient from an existing Idevice connection
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`socket`] - An IdeviceSocket handle
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated NotificationProxyClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `socket` must be a valid pointer to a handle allocated by this library. The socket is consumed,
|
||||||
|
/// and may not be used again.
|
||||||
|
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_new(
|
||||||
|
socket: *mut IdeviceHandle,
|
||||||
|
client: *mut *mut NotificationProxyClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if socket.is_null() || client.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
let socket = unsafe { Box::from_raw(socket) }.0;
|
||||||
|
let r = NotificationProxyClient::new(socket);
|
||||||
|
let boxed = Box::new(NotificationProxyClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posts a notification to the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid NotificationProxyClient handle
|
||||||
|
/// * `name` - C string containing the notification name
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `name` must be a valid null-terminated C string
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_post(
|
||||||
|
client: *mut NotificationProxyClientHandle,
|
||||||
|
name: *const c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || name.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
|
||||||
|
Ok(s) => s.to_string(),
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.post_notification(name_str).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => null_mut(),
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observes a specific notification
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid NotificationProxyClient handle
|
||||||
|
/// * `name` - C string containing the notification name to observe
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `name` must be a valid null-terminated C string
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_observe(
|
||||||
|
client: *mut NotificationProxyClientHandle,
|
||||||
|
name: *const c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || name.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
|
||||||
|
Ok(s) => s.to_string(),
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.observe_notification(name_str).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => null_mut(),
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observes multiple notifications at once
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid NotificationProxyClient handle
|
||||||
|
/// * `names` - A null-terminated array of C strings containing notification names
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `names` must be a valid pointer to a null-terminated array of null-terminated C strings
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_observe_multiple(
|
||||||
|
client: *mut NotificationProxyClientHandle,
|
||||||
|
names: *const *const c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || names.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut notification_names: Vec<String> = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
loop {
|
||||||
|
let ptr = unsafe { *names.add(i) };
|
||||||
|
if ptr.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match unsafe { CStr::from_ptr(ptr) }.to_str() {
|
||||||
|
Ok(s) => notification_names.push(s.to_string()),
|
||||||
|
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refs: Vec<&str> = notification_names.iter().map(|s| s.as_str()).collect();
|
||||||
|
|
||||||
|
let res: Result<(), IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.observe_notifications(&refs).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(_) => null_mut(),
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives the next notification from the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid NotificationProxyClient handle
|
||||||
|
/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string`
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_receive(
|
||||||
|
client: *mut NotificationProxyClientHandle,
|
||||||
|
name_out: *mut *mut c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || name_out.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<String, IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.receive_notification().await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(name) => match CString::new(name) {
|
||||||
|
Ok(c_string) => {
|
||||||
|
unsafe { *name_out = c_string.into_raw() };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(_) => ffi_err!(IdeviceError::FfiInvalidString),
|
||||||
|
},
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives the next notification with a timeout
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid NotificationProxyClient handle
|
||||||
|
/// * `interval` - Timeout in seconds to wait for a notification
|
||||||
|
/// * `name_out` - On success, will be set to a newly allocated C string containing the notification name
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `name_out` must be a valid pointer. The returned string must be freed with `notification_proxy_free_string`
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_receive_with_timeout(
|
||||||
|
client: *mut NotificationProxyClientHandle,
|
||||||
|
interval: u64,
|
||||||
|
name_out: *mut *mut c_char,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || name_out.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<String, IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.receive_notification_with_timeout(interval).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(name) => match CString::new(name) {
|
||||||
|
Ok(c_string) => {
|
||||||
|
unsafe { *name_out = c_string.into_raw() };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(_) => ffi_err!(IdeviceError::FfiInvalidString),
|
||||||
|
},
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees a string returned by notification_proxy_receive
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `s` must be a valid pointer returned from `notification_proxy_receive`
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_free_string(s: *mut c_char) {
|
||||||
|
if !s.is_null() {
|
||||||
|
let _ = unsafe { CString::from_raw(s) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees a handle
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - The handle to free
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `handle` must be a valid pointer to the handle that was allocated by this library,
|
||||||
|
/// or NULL (in which case this function does nothing)
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn notification_proxy_client_free(
|
||||||
|
handle: *mut NotificationProxyClientHandle,
|
||||||
|
) {
|
||||||
|
if !handle.is_null() {
|
||||||
|
tracing::debug!("Freeing notification_proxy_client");
|
||||||
|
let _ = unsafe { Box::from_raw(handle) };
|
||||||
|
}
|
||||||
|
}
|
||||||
132
ffi/src/screenshotr.rs
Normal file
132
ffi/src/screenshotr.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceError, IdeviceService, provider::IdeviceProvider,
|
||||||
|
services::screenshotr::ScreenshotService,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{IdeviceFfiError, ffi_err, provider::IdeviceProviderHandle, run_sync_local};
|
||||||
|
|
||||||
|
pub struct ScreenshotrClientHandle(pub ScreenshotService);
|
||||||
|
|
||||||
|
/// Represents a screenshot data buffer
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct ScreenshotData {
|
||||||
|
pub data: *mut u8,
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to screenshotr service using provider
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`provider`] - An IdeviceProvider
|
||||||
|
/// * [`client`] - On success, will be set to point to a newly allocated ScreenshotrClient handle
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `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
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_connect(
|
||||||
|
provider: *mut IdeviceProviderHandle,
|
||||||
|
client: *mut *mut ScreenshotrClientHandle,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if provider.is_null() || client.is_null() {
|
||||||
|
tracing::error!("Null pointer provided");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<ScreenshotService, IdeviceError> = run_sync_local(async move {
|
||||||
|
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
|
||||||
|
ScreenshotService::connect(provider_ref).await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let boxed = Box::new(ScreenshotrClientHandle(r));
|
||||||
|
unsafe { *client = Box::into_raw(boxed) };
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a screenshot from the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid ScreenshotrClient handle
|
||||||
|
/// * `screenshot` - Pointer to store the screenshot data
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `screenshot` must be a valid pointer to store the screenshot data
|
||||||
|
/// The caller is responsible for freeing the screenshot data using screenshotr_screenshot_free
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_take_screenshot(
|
||||||
|
client: *mut ScreenshotrClientHandle,
|
||||||
|
screenshot: *mut ScreenshotData,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || screenshot.is_null() {
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async move {
|
||||||
|
let client_ref = unsafe { &mut (*client).0 };
|
||||||
|
client_ref.take_screenshot().await
|
||||||
|
});
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(data) => {
|
||||||
|
let len = data.len();
|
||||||
|
let boxed = data.into_boxed_slice();
|
||||||
|
let ptr = Box::into_raw(boxed) as *mut u8;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
(*screenshot).data = ptr;
|
||||||
|
(*screenshot).length = len;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees screenshot data
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `screenshot` - The screenshot data to free
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `screenshot` must be a valid ScreenshotData that was allocated by screenshotr_take_screenshot
|
||||||
|
/// or NULL (in which case this function does nothing)
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_screenshot_free(screenshot: ScreenshotData) {
|
||||||
|
if !screenshot.data.is_null() && screenshot.length > 0 {
|
||||||
|
tracing::debug!("Freeing screenshot data");
|
||||||
|
let _ =
|
||||||
|
unsafe { Vec::from_raw_parts(screenshot.data, screenshot.length, screenshot.length) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frees a ScreenshotrClient handle
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * [`handle`] - The handle to free
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `handle` must be a valid pointer to the handle that was allocated by this library,
|
||||||
|
/// or NULL (in which case this function does nothing)
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn screenshotr_client_free(handle: *mut ScreenshotrClientHandle) {
|
||||||
|
if !handle.is_null() {
|
||||||
|
tracing::debug!("Freeing screenshotr_client");
|
||||||
|
let _ = unsafe { Box::from_raw(handle) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use idevice::{
|
|||||||
IdeviceError, IdeviceService, provider::IdeviceProvider,
|
IdeviceError, IdeviceService, provider::IdeviceProvider,
|
||||||
springboardservices::SpringBoardServicesClient,
|
springboardservices::SpringBoardServicesClient,
|
||||||
};
|
};
|
||||||
|
use plist_ffi::plist_t;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync,
|
IdeviceFfiError, IdeviceHandle, ffi_err, provider::IdeviceProviderHandle, run_sync,
|
||||||
@@ -137,6 +138,169 @@ pub unsafe extern "C" fn springboard_services_get_icon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the home screen wallpaper preview as PNG image
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid SpringBoardServicesClient handle
|
||||||
|
/// * `out_result` - On success, will be set to point to newly allocated png image
|
||||||
|
/// * `out_result_len` - On success, will contain the size of the data in bytes
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `out_result` and `out_result_len` must be valid, non-null pointers
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn springboard_services_get_home_screen_wallpaper_preview(
|
||||||
|
client: *mut SpringBoardServicesClientHandle,
|
||||||
|
out_result: *mut *mut c_void,
|
||||||
|
out_result_len: *mut libc::size_t,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || out_result.is_null() || out_result_len.is_null() {
|
||||||
|
tracing::error!("Invalid arguments: {client:?}, {out_result:?}");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
let client = unsafe { &mut *client };
|
||||||
|
|
||||||
|
let res: Result<Vec<u8>, IdeviceError> =
|
||||||
|
run_sync(async { client.0.get_home_screen_wallpaper_preview_pngdata().await });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let len = r.len();
|
||||||
|
let boxed_slice = r.into_boxed_slice();
|
||||||
|
let ptr = boxed_slice.as_ptr();
|
||||||
|
std::mem::forget(boxed_slice);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*out_result = ptr as *mut c_void;
|
||||||
|
*out_result_len = len;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the lock screen wallpaper preview as PNG image
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid SpringBoardServicesClient handle
|
||||||
|
/// * `out_result` - On success, will be set to point to newly allocated png image
|
||||||
|
/// * `out_result_len` - On success, will contain the size of the data in bytes
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `out_result` and `out_result_len` must be valid, non-null pointers
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn springboard_services_get_lock_screen_wallpaper_preview(
|
||||||
|
client: *mut SpringBoardServicesClientHandle,
|
||||||
|
out_result: *mut *mut c_void,
|
||||||
|
out_result_len: *mut libc::size_t,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || out_result.is_null() || out_result_len.is_null() {
|
||||||
|
tracing::error!("Invalid arguments: {client:?}, {out_result:?}");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
let client = unsafe { &mut *client };
|
||||||
|
|
||||||
|
let res: Result<Vec<u8>, IdeviceError> =
|
||||||
|
run_sync(async { client.0.get_lock_screen_wallpaper_preview_pngdata().await });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(r) => {
|
||||||
|
let len = r.len();
|
||||||
|
let boxed_slice = r.into_boxed_slice();
|
||||||
|
let ptr = boxed_slice.as_ptr();
|
||||||
|
std::mem::forget(boxed_slice);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*out_result = ptr as *mut c_void;
|
||||||
|
*out_result_len = len;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current interface orientation of the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid SpringBoardServicesClient handle
|
||||||
|
/// * `out_orientation` - On success, will contain the orientation value (0-4)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `out_orientation` must be a valid, non-null pointer
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn springboard_services_get_interface_orientation(
|
||||||
|
client: *mut SpringBoardServicesClientHandle,
|
||||||
|
out_orientation: *mut u8,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || out_orientation.is_null() {
|
||||||
|
tracing::error!("Invalid arguments: {client:?}, {out_orientation:?}");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
let client = unsafe { &mut *client };
|
||||||
|
|
||||||
|
let res = run_sync(async { client.0.get_interface_orientation().await });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(orientation) => {
|
||||||
|
unsafe {
|
||||||
|
*out_orientation = orientation as u8;
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the home screen icon layout metrics
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `client` - A valid SpringBoardServicesClient handle
|
||||||
|
/// * `res` - On success, will point to a plist dictionary node containing the metrics
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// An IdeviceFfiError on error, null on success
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `client` must be a valid pointer to a handle allocated by this library
|
||||||
|
/// `res` must be a valid, non-null pointer
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn springboard_services_get_homescreen_icon_metrics(
|
||||||
|
client: *mut SpringBoardServicesClientHandle,
|
||||||
|
res: *mut plist_t,
|
||||||
|
) -> *mut IdeviceFfiError {
|
||||||
|
if client.is_null() || res.is_null() {
|
||||||
|
tracing::error!("Invalid arguments: {client:?}, {res:?}");
|
||||||
|
return ffi_err!(IdeviceError::FfiInvalidArg);
|
||||||
|
}
|
||||||
|
let client = unsafe { &mut *client };
|
||||||
|
|
||||||
|
let output = run_sync(async { client.0.get_homescreen_icon_metrics().await });
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(metrics) => {
|
||||||
|
unsafe {
|
||||||
|
*res =
|
||||||
|
plist_ffi::PlistWrapper::new_node(plist::Value::Dictionary(metrics)).into_ptr();
|
||||||
|
}
|
||||||
|
null_mut()
|
||||||
|
}
|
||||||
|
Err(e) => ffi_err!(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Frees an SpringBoardServicesClient handle
|
/// Frees an SpringBoardServicesClient handle
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -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.50"
|
version = "0.1.53"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
documentation = "https://docs.rs/idevice"
|
documentation = "https://docs.rs/idevice"
|
||||||
@@ -21,6 +21,7 @@ tokio-openssl = { version = "0.6", optional = true }
|
|||||||
openssl = { version = "0.10", optional = true }
|
openssl = { version = "0.10", optional = true }
|
||||||
|
|
||||||
plist = { version = "1.8" }
|
plist = { version = "1.8" }
|
||||||
|
plist-macro = { version = "0.1.3" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
ns-keyed-archive = { version = "0.1.4", optional = true }
|
ns-keyed-archive = { version = "0.1.4", optional = true }
|
||||||
crossfire = { version = "2.1", optional = true }
|
crossfire = { version = "2.1", optional = true }
|
||||||
@@ -40,7 +41,7 @@ json = { version = "0.12", optional = true }
|
|||||||
byteorder = { version = "1.5", optional = true }
|
byteorder = { version = "1.5", optional = true }
|
||||||
bytes = { version = "1.10", optional = true }
|
bytes = { version = "1.10", optional = true }
|
||||||
|
|
||||||
reqwest = { version = "0.12", features = [
|
reqwest = { version = "0.13", features = [
|
||||||
"json",
|
"json",
|
||||||
], optional = true, default-features = false }
|
], optional = true, default-features = false }
|
||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.9", optional = true }
|
||||||
@@ -76,7 +77,7 @@ ring = ["rustls", "rustls/ring", "tokio-rustls/ring"]
|
|||||||
rustls = ["dep:rustls", "dep:tokio-rustls"]
|
rustls = ["dep:rustls", "dep:tokio-rustls"]
|
||||||
openssl = ["dep:openssl", "dep:tokio-openssl"]
|
openssl = ["dep:openssl", "dep:tokio-openssl"]
|
||||||
|
|
||||||
afc = ["dep:chrono"]
|
afc = ["dep:chrono", "dep:futures"]
|
||||||
amfi = []
|
amfi = []
|
||||||
bt_packet_logger = []
|
bt_packet_logger = []
|
||||||
companion_proxy = []
|
companion_proxy = []
|
||||||
@@ -101,6 +102,7 @@ misagent = []
|
|||||||
mobile_image_mounter = ["dep:sha2"]
|
mobile_image_mounter = ["dep:sha2"]
|
||||||
mobileactivationd = ["dep:reqwest"]
|
mobileactivationd = ["dep:reqwest"]
|
||||||
mobilebackup2 = []
|
mobilebackup2 = []
|
||||||
|
notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"]
|
||||||
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"]
|
||||||
pcapd = []
|
pcapd = []
|
||||||
@@ -152,6 +154,7 @@ full = [
|
|||||||
"mobile_image_mounter",
|
"mobile_image_mounter",
|
||||||
"mobileactivationd",
|
"mobileactivationd",
|
||||||
"mobilebackup2",
|
"mobilebackup2",
|
||||||
|
"notification_proxy",
|
||||||
"pair",
|
"pair",
|
||||||
"pcapd",
|
"pcapd",
|
||||||
"preboard_service",
|
"preboard_service",
|
||||||
|
|||||||
334
idevice/src/cursor.rs
Normal file
334
idevice/src/cursor.rs
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Cursor<'a> {
|
||||||
|
inner: &'a [u8],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cursor<'a> {
|
||||||
|
/// Creates a new cursor
|
||||||
|
pub fn new(inner: &'a [u8]) -> Self {
|
||||||
|
Self { inner, pos: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.inner.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at_end(&self) -> bool {
|
||||||
|
self.pos == self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&mut self, to_read: usize) -> Option<&'a [u8]> {
|
||||||
|
// Check if the end of the slice (self.pos + to_read) is beyond the buffer length
|
||||||
|
if self
|
||||||
|
.pos
|
||||||
|
.checked_add(to_read)
|
||||||
|
.is_none_or(|end_pos| end_pos > self.inner.len())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The end of the slice is self.pos + to_read
|
||||||
|
let end_pos = self.pos + to_read;
|
||||||
|
let res = Some(&self.inner[self.pos..end_pos]);
|
||||||
|
self.pos = end_pos;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn back(&mut self, to_back: usize) {
|
||||||
|
let to_back = if to_back > self.pos {
|
||||||
|
self.pos
|
||||||
|
} else {
|
||||||
|
to_back
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pos -= to_back;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if actually all zeroes
|
||||||
|
pub fn read_assert_zero(&mut self, to_read: usize) -> Option<()> {
|
||||||
|
let bytes = self.read(to_read)?;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
for b in bytes.iter() {
|
||||||
|
if *b > 0 {
|
||||||
|
eprintln!("Zero read contained non-zero values!");
|
||||||
|
eprintln!("{bytes:02X?}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_to(&mut self, end: usize) -> Option<&'a [u8]> {
|
||||||
|
if end > self.inner.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let res = Some(&self.inner[self.pos..end]);
|
||||||
|
self.pos = end;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek_to(&mut self, end: usize) -> Option<&'a [u8]> {
|
||||||
|
if end > self.inner.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(&self.inner[self.pos..end])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek(&self, to_read: usize) -> Option<&'a [u8]> {
|
||||||
|
if self
|
||||||
|
.pos
|
||||||
|
.checked_add(to_read)
|
||||||
|
.is_none_or(|end_pos| end_pos > self.inner.len())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end_pos = self.pos + to_read;
|
||||||
|
Some(&self.inner[self.pos..end_pos])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reveal(&self, surrounding: usize) {
|
||||||
|
let len = self.inner.len();
|
||||||
|
|
||||||
|
if self.pos > len {
|
||||||
|
println!("Cursor is past end of buffer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = self.pos.saturating_sub(surrounding);
|
||||||
|
let end = (self.pos + surrounding + 1).min(len);
|
||||||
|
|
||||||
|
// HEADER
|
||||||
|
println!("Reveal around pos {} ({} bytes):", self.pos, surrounding);
|
||||||
|
|
||||||
|
// --- HEX LINE ---
|
||||||
|
print!("Hex: ");
|
||||||
|
for i in start..end {
|
||||||
|
if i == self.pos {
|
||||||
|
print!("[{:02X}] ", self.inner[i]);
|
||||||
|
} else {
|
||||||
|
print!("{:02X} ", self.inner[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// --- ASCII LINE ---
|
||||||
|
print!("Ascii: ");
|
||||||
|
for i in start..end {
|
||||||
|
let b = self.inner[i];
|
||||||
|
let c = if b.is_ascii_graphic() || b == b' ' {
|
||||||
|
b as char
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
};
|
||||||
|
|
||||||
|
if i == self.pos {
|
||||||
|
print!("[{}] ", c);
|
||||||
|
} else {
|
||||||
|
print!("{} ", c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// --- OFFSET LINE ---
|
||||||
|
print!("Offset: ");
|
||||||
|
for i in start..end {
|
||||||
|
let off = i as isize - self.pos as isize;
|
||||||
|
if i == self.pos {
|
||||||
|
print!("[{}] ", off);
|
||||||
|
} else {
|
||||||
|
print!("{:<3} ", off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remaining(&mut self) -> &'a [u8] {
|
||||||
|
let res = &self.inner[self.pos..];
|
||||||
|
self.pos = self.inner.len();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_u8(&mut self) -> Option<u8> {
|
||||||
|
if self.pos == self.inner.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let res = Some(self.inner[self.pos]);
|
||||||
|
self.pos += 1;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_u16(&mut self) -> Option<u16> {
|
||||||
|
const SIZE: usize = 2;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u16::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_u16(&mut self) -> Option<u16> {
|
||||||
|
const SIZE: usize = 2;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u16::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_u32(&mut self) -> Option<u32> {
|
||||||
|
const SIZE: usize = 4;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u32::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_u32(&mut self) -> Option<u32> {
|
||||||
|
const SIZE: usize = 4;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u32::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_u64(&mut self) -> Option<u64> {
|
||||||
|
const SIZE: usize = 8;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u64::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_u64(&mut self) -> Option<u64> {
|
||||||
|
const SIZE: usize = 8;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u64::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_u128(&mut self) -> Option<u128> {
|
||||||
|
const SIZE: usize = 16;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u128::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_u128(&mut self) -> Option<u128> {
|
||||||
|
const SIZE: usize = 16;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(u128::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_f32(&mut self) -> Option<f32> {
|
||||||
|
const SIZE: usize = 4;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(f32::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_f32(&mut self) -> Option<f32> {
|
||||||
|
const SIZE: usize = 4;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(f32::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_i8(&mut self) -> Option<i8> {
|
||||||
|
if self.pos == self.inner.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let res = Some(self.inner[self.pos]).map(|x| x as i8);
|
||||||
|
self.pos += 1;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_i16(&mut self) -> Option<i16> {
|
||||||
|
const SIZE: usize = 2;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i16::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_i16(&mut self) -> Option<i16> {
|
||||||
|
const SIZE: usize = 2;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i16::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_i32(&mut self) -> Option<i32> {
|
||||||
|
const SIZE: usize = 4;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i32::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_i32(&mut self) -> Option<i32> {
|
||||||
|
const SIZE: usize = 4;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i32::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_i64(&mut self) -> Option<i64> {
|
||||||
|
const SIZE: usize = 8;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i64::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_i64(&mut self) -> Option<i64> {
|
||||||
|
const SIZE: usize = 8;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i64::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_le_i128(&mut self) -> Option<i128> {
|
||||||
|
const SIZE: usize = 16;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i128::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_be_i128(&mut self) -> Option<i128> {
|
||||||
|
const SIZE: usize = 16;
|
||||||
|
let bytes = self.read(SIZE)?;
|
||||||
|
let bytes: [u8; SIZE] = bytes.try_into().unwrap();
|
||||||
|
Some(i128::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_2(&mut self) -> Option<[u8; 2]> {
|
||||||
|
let bytes = self.read(2)?;
|
||||||
|
Some(bytes.to_owned().try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_3(&mut self) -> Option<[u8; 3]> {
|
||||||
|
let bytes = self.read(3)?;
|
||||||
|
Some(bytes.to_owned().try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_4(&mut self) -> Option<[u8; 4]> {
|
||||||
|
let bytes = self.read(4)?;
|
||||||
|
Some(bytes.to_owned().try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_8(&mut self) -> Option<[u8; 8]> {
|
||||||
|
let bytes = self.read(8)?;
|
||||||
|
Some(bytes.to_owned().try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_20(&mut self) -> Option<[u8; 20]> {
|
||||||
|
let bytes = self.read(20)?;
|
||||||
|
Some(bytes.to_owned().try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_32(&mut self) -> Option<[u8; 32]> {
|
||||||
|
let bytes = self.read(32)?;
|
||||||
|
Some(bytes.to_owned().try_into().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
|
|
||||||
#[cfg(all(feature = "pair", feature = "rustls"))]
|
#[cfg(all(feature = "pair", feature = "rustls"))]
|
||||||
mod ca;
|
mod ca;
|
||||||
|
pub mod cursor;
|
||||||
|
mod obfuscation;
|
||||||
pub mod pairing_file;
|
pub mod pairing_file;
|
||||||
pub mod plist_macro;
|
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
#[cfg(feature = "remote_pairing")]
|
#[cfg(feature = "remote_pairing")]
|
||||||
pub mod remote_pairing;
|
pub mod remote_pairing;
|
||||||
@@ -20,7 +21,6 @@ pub mod tss;
|
|||||||
pub mod tunneld;
|
pub mod tunneld;
|
||||||
#[cfg(feature = "usbmuxd")]
|
#[cfg(feature = "usbmuxd")]
|
||||||
pub mod usbmuxd;
|
pub mod usbmuxd;
|
||||||
mod util;
|
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
#[cfg(feature = "xpc")]
|
#[cfg(feature = "xpc")]
|
||||||
pub mod xpc;
|
pub mod xpc;
|
||||||
@@ -31,6 +31,7 @@ pub use services::*;
|
|||||||
#[cfg(feature = "xpc")]
|
#[cfg(feature = "xpc")]
|
||||||
pub use xpc::RemoteXpcClient;
|
pub use xpc::RemoteXpcClient;
|
||||||
|
|
||||||
|
use plist_macro::{plist, pretty_print_dictionary, pretty_print_plist};
|
||||||
use provider::{IdeviceProvider, RsdProvider};
|
use provider::{IdeviceProvider, RsdProvider};
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
use rustls::{crypto::CryptoProvider, pki_types::ServerName};
|
use rustls::{crypto::CryptoProvider, pki_types::ServerName};
|
||||||
@@ -40,9 +41,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
pub use util::{pretty_print_dictionary, pretty_print_plist};
|
|
||||||
|
|
||||||
use crate::services::lockdown::LockdownClient;
|
use crate::services::lockdown::LockdownClient;
|
||||||
|
|
||||||
@@ -79,6 +78,7 @@ pub trait IdeviceService: Sized {
|
|||||||
async fn connect(provider: &dyn IdeviceProvider) -> Result<Self, IdeviceError> {
|
async fn connect(provider: &dyn IdeviceProvider) -> Result<Self, IdeviceError> {
|
||||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
let legacy = lockdown
|
let legacy = lockdown
|
||||||
.get_value(Some("ProductVersion"), None)
|
.get_value(Some("ProductVersion"), None)
|
||||||
.await
|
.await
|
||||||
@@ -90,6 +90,9 @@ pub trait IdeviceService: Sized {
|
|||||||
.map(|x| x < 5)
|
.map(|x| x < 5)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "openssl"))]
|
||||||
|
let legacy = false;
|
||||||
|
|
||||||
lockdown
|
lockdown
|
||||||
.start_session(&provider.get_pairing_file().await?)
|
.start_session(&provider.get_pairing_file().await?)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -194,7 +197,7 @@ impl Idevice {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns `IdeviceError` if communication fails or response is invalid
|
/// Returns `IdeviceError` if communication fails or response is invalid
|
||||||
pub async fn get_type(&mut self) -> Result<String, IdeviceError> {
|
pub async fn get_type(&mut self) -> Result<String, IdeviceError> {
|
||||||
let req = crate::plist!({
|
let req = plist!({
|
||||||
"Label": self.label.clone(),
|
"Label": self.label.clone(),
|
||||||
"Request": "QueryType",
|
"Request": "QueryType",
|
||||||
});
|
});
|
||||||
@@ -214,7 +217,7 @@ impl Idevice {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns `IdeviceError` if the protocol sequence isn't followed correctly
|
/// Returns `IdeviceError` if the protocol sequence isn't followed correctly
|
||||||
pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> {
|
pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> {
|
||||||
let req = crate::plist!({
|
let req = plist!({
|
||||||
"Label": self.label.clone(),
|
"Label": self.label.clone(),
|
||||||
"ProtocolVersion": "2",
|
"ProtocolVersion": "2",
|
||||||
"Request": "RSDCheckin",
|
"Request": "RSDCheckin",
|
||||||
@@ -484,7 +487,13 @@ impl Idevice {
|
|||||||
if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) {
|
if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) {
|
||||||
return Err(e);
|
return Err(e);
|
||||||
} else {
|
} else {
|
||||||
return Err(IdeviceError::UnknownErrorType(e));
|
let msg =
|
||||||
|
if let Some(desc) = res.get("ErrorDescription").and_then(|x| x.as_string()) {
|
||||||
|
format!("{} ({})", e, desc)
|
||||||
|
} else {
|
||||||
|
e
|
||||||
|
};
|
||||||
|
return Err(IdeviceError::UnknownErrorType(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(res)
|
Ok(res)
|
||||||
@@ -875,6 +884,9 @@ pub enum IdeviceError {
|
|||||||
#[cfg(feature = "remote_pairing")]
|
#[cfg(feature = "remote_pairing")]
|
||||||
#[error("Chacha encryption error")]
|
#[error("Chacha encryption error")]
|
||||||
ChachaEncryption(chacha20poly1305::Error) = -75,
|
ChachaEncryption(chacha20poly1305::Error) = -75,
|
||||||
|
#[cfg(feature = "notification_proxy")]
|
||||||
|
#[error("notification proxy died")]
|
||||||
|
NotificationProxyDeath = -76,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdeviceError {
|
impl IdeviceError {
|
||||||
@@ -1048,6 +1060,9 @@ impl IdeviceError {
|
|||||||
IdeviceError::PairVerifyFailed => -73,
|
IdeviceError::PairVerifyFailed => -73,
|
||||||
IdeviceError::SrpAuthFailed => -74,
|
IdeviceError::SrpAuthFailed => -74,
|
||||||
IdeviceError::ChachaEncryption(_) => -75,
|
IdeviceError::ChachaEncryption(_) => -75,
|
||||||
|
|
||||||
|
#[cfg(feature = "notification_proxy")]
|
||||||
|
IdeviceError::NotificationProxyDeath => -76,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
idevice/src/obfuscation.rs
Normal file
15
idevice/src/obfuscation.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! obf {
|
||||||
|
($lit:literal) => {{
|
||||||
|
#[cfg(feature = "obfuscate")]
|
||||||
|
{
|
||||||
|
std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "obfuscate"))]
|
||||||
|
{
|
||||||
|
std::borrow::Cow::Borrowed($lit)
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ pub struct PairingFile {
|
|||||||
/// Host identifier
|
/// Host identifier
|
||||||
pub host_id: String,
|
pub host_id: String,
|
||||||
/// Escrow bag allowing for access while locked
|
/// Escrow bag allowing for access while locked
|
||||||
pub escrow_bag: Vec<u8>,
|
pub escrow_bag: Option<Vec<u8>>,
|
||||||
/// Device's WiFi MAC address
|
/// Device's WiFi MAC address
|
||||||
pub wifi_mac_address: String,
|
pub wifi_mac_address: String,
|
||||||
/// Device's Unique Device Identifier (optional)
|
/// Device's Unique Device Identifier (optional)
|
||||||
@@ -73,7 +73,7 @@ struct RawPairingFile {
|
|||||||
system_buid: String,
|
system_buid: String,
|
||||||
#[serde(rename = "HostID")]
|
#[serde(rename = "HostID")]
|
||||||
host_id: String,
|
host_id: String,
|
||||||
escrow_bag: Data,
|
escrow_bag: Option<Data>, // None on Apple Watch
|
||||||
#[serde(rename = "WiFiMACAddress")]
|
#[serde(rename = "WiFiMACAddress")]
|
||||||
wifi_mac_address: String,
|
wifi_mac_address: String,
|
||||||
#[serde(rename = "UDID")]
|
#[serde(rename = "UDID")]
|
||||||
@@ -206,7 +206,7 @@ impl TryFrom<RawPairingFile> for PairingFile {
|
|||||||
root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?,
|
root_certificate: CertificateDer::from_pem_slice(&root_certificate_pem)?,
|
||||||
system_buid: value.system_buid,
|
system_buid: value.system_buid,
|
||||||
host_id: value.host_id,
|
host_id: value.host_id,
|
||||||
escrow_bag: value.escrow_bag.into(),
|
escrow_bag: value.escrow_bag.map(|x| x.into()),
|
||||||
wifi_mac_address: value.wifi_mac_address,
|
wifi_mac_address: value.wifi_mac_address,
|
||||||
udid: value.udid,
|
udid: value.udid,
|
||||||
})
|
})
|
||||||
@@ -230,7 +230,7 @@ impl TryFrom<RawPairingFile> for PairingFile {
|
|||||||
root_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.root_certificate))?,
|
root_certificate: X509::from_pem(&Into::<Vec<u8>>::into(value.root_certificate))?,
|
||||||
system_buid: value.system_buid,
|
system_buid: value.system_buid,
|
||||||
host_id: value.host_id,
|
host_id: value.host_id,
|
||||||
escrow_bag: value.escrow_bag.into(),
|
escrow_bag: value.escrow_bag.map(|x| x.into()),
|
||||||
wifi_mac_address: value.wifi_mac_address,
|
wifi_mac_address: value.wifi_mac_address,
|
||||||
udid: value.udid,
|
udid: value.udid,
|
||||||
})
|
})
|
||||||
@@ -258,7 +258,7 @@ impl From<PairingFile> for RawPairingFile {
|
|||||||
root_certificate: Data::new(root_cert_data),
|
root_certificate: Data::new(root_cert_data),
|
||||||
system_buid: value.system_buid,
|
system_buid: value.system_buid,
|
||||||
host_id: value.host_id.clone(),
|
host_id: value.host_id.clone(),
|
||||||
escrow_bag: Data::new(value.escrow_bag),
|
escrow_bag: value.escrow_bag.map(Data::new),
|
||||||
wifi_mac_address: value.wifi_mac_address,
|
wifi_mac_address: value.wifi_mac_address,
|
||||||
udid: value.udid,
|
udid: value.udid,
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ impl TryFrom<PairingFile> for RawPairingFile {
|
|||||||
root_certificate: Data::new(value.root_certificate.to_pem()?),
|
root_certificate: Data::new(value.root_certificate.to_pem()?),
|
||||||
system_buid: value.system_buid,
|
system_buid: value.system_buid,
|
||||||
host_id: value.host_id.clone(),
|
host_id: value.host_id.clone(),
|
||||||
escrow_bag: Data::new(value.escrow_bag),
|
escrow_bag: value.escrow_bag.map(Data::new),
|
||||||
wifi_mac_address: value.wifi_mac_address,
|
wifi_mac_address: value.wifi_mac_address,
|
||||||
udid: value.udid,
|
udid: value.udid,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,714 +0,0 @@
|
|||||||
// Jackson Coxson
|
|
||||||
// Ported from serde's json!
|
|
||||||
|
|
||||||
/// Construct a `plist::Value` from a JSON-like literal.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use idevice::plist;
|
|
||||||
/// #
|
|
||||||
/// let value = plist!({
|
|
||||||
/// "code": 200,
|
|
||||||
/// "success": true,
|
|
||||||
/// "payload": {
|
|
||||||
/// "features": [
|
|
||||||
/// "serde",
|
|
||||||
/// "plist"
|
|
||||||
/// ],
|
|
||||||
/// "homepage": null
|
|
||||||
/// }
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Variables or expressions can be interpolated into the plist literal. Any type
|
|
||||||
/// interpolated into an array element or object value must implement `Into<plist::Value>`.
|
|
||||||
/// If the conversion fails, the `plist!` macro will panic.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use idevice::plist;
|
|
||||||
/// #
|
|
||||||
/// let code = 200;
|
|
||||||
/// let features = vec!["serde", "plist"];
|
|
||||||
///
|
|
||||||
/// let value = plist!({
|
|
||||||
/// "code": code,
|
|
||||||
/// "success": code == 200,
|
|
||||||
/// "payload": {
|
|
||||||
/// features[0]: features[1]
|
|
||||||
/// }
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Trailing commas are allowed inside both arrays and objects.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use idevice::plist;
|
|
||||||
/// #
|
|
||||||
/// let value = plist!([
|
|
||||||
/// "notice",
|
|
||||||
/// "the",
|
|
||||||
/// "trailing",
|
|
||||||
/// "comma -->",
|
|
||||||
/// ]);
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! plist {
|
|
||||||
// Force: dictionary out
|
|
||||||
(dict { $($tt:tt)+ }) => {{
|
|
||||||
let mut object = plist::Dictionary::new();
|
|
||||||
$crate::plist_internal!(@object object () ($($tt)+) ($($tt)+));
|
|
||||||
object
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Force: value out (explicit, though default already does this)
|
|
||||||
(value { $($tt:tt)+ }) => {
|
|
||||||
$crate::plist_internal!({ $($tt)+ })
|
|
||||||
};
|
|
||||||
|
|
||||||
// Force: raw vec of plist::Value out
|
|
||||||
(array [ $($tt:tt)+ ]) => {
|
|
||||||
$crate::plist_internal!(@array [] $($tt)+)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hide distracting implementation details from the generated rustdoc.
|
|
||||||
($($plist:tt)+) => {
|
|
||||||
$crate::plist_internal!($($plist)+)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
#[doc(hidden)]
|
|
||||||
macro_rules! plist_internal {
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
// TT muncher for parsing the inside of an array [...]. Produces a vec![...]
|
|
||||||
// of the elements.
|
|
||||||
//
|
|
||||||
// Must be invoked as: plist_internal!(@array [] $($tt)*)
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Done with trailing comma.
|
|
||||||
(@array [$($elems:expr,)*]) => {
|
|
||||||
vec![$($elems,)*]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Done without trailing comma.
|
|
||||||
(@array [$($elems:expr),*]) => {
|
|
||||||
vec![$($elems),*]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next element is `null`.
|
|
||||||
(@array [$($elems:expr,)*] null $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(null)] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next element is `true`.
|
|
||||||
(@array [$($elems:expr,)*] true $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(true)] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next element is `false`.
|
|
||||||
(@array [$($elems:expr,)*] false $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!(false)] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next element is an array.
|
|
||||||
(@array [$($elems:expr,)*] [$($array:tt)*] $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!([$($array)*])] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next element is a map.
|
|
||||||
(@array [$($elems:expr,)*] {$($map:tt)*} $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!({$($map)*})] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next element is an expression followed by comma.
|
|
||||||
(@array [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($next),] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Last element is an expression with no trailing comma.
|
|
||||||
(@array [$($elems:expr,)*] $last:expr) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* $crate::plist_internal!($last)])
|
|
||||||
};
|
|
||||||
|
|
||||||
// Comma after the most recent element.
|
|
||||||
(@array [$($elems:expr),*] , $($rest:tt)*) => {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)*] $($rest)*)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Unexpected token after most recent element.
|
|
||||||
(@array [$($elems:expr),*] $unexpected:tt $($rest:tt)*) => {
|
|
||||||
$crate::plist_unexpected!($unexpected)
|
|
||||||
};
|
|
||||||
|
|
||||||
(@array [$($elems:expr,)*] ? $maybe:expr , $($rest:tt)*) => {
|
|
||||||
if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* __v,] $($rest)*)
|
|
||||||
} else {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)*] $($rest)*)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(@array [$($elems:expr,)*] ? $maybe:expr) => {
|
|
||||||
if let Some(__v) = $crate::plist_macro::plist_maybe($maybe) {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)* __v])
|
|
||||||
} else {
|
|
||||||
$crate::plist_internal!(@array [$($elems,)*])
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
// TT muncher for parsing the inside of an object {...}. Each entry is
|
|
||||||
// inserted into the given map variable.
|
|
||||||
//
|
|
||||||
// Must be invoked as: plist_internal!(@object $map () ($($tt)*) ($($tt)*))
|
|
||||||
//
|
|
||||||
// We require two copies of the input tokens so that we can match on one
|
|
||||||
// copy and trigger errors on the other copy.
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Done.
|
|
||||||
(@object $object:ident () () ()) => {};
|
|
||||||
|
|
||||||
// Insert the current entry followed by trailing comma.
|
|
||||||
(@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
|
|
||||||
let _ = $object.insert(($($key)+).into(), $value);
|
|
||||||
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Current entry followed by unexpected token.
|
|
||||||
(@object $object:ident [$($key:tt)+] ($value:expr) $unexpected:tt $($rest:tt)*) => {
|
|
||||||
$crate::plist_unexpected!($unexpected);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Insert the last entry without trailing comma.
|
|
||||||
(@object $object:ident [$($key:tt)+] ($value:expr)) => {
|
|
||||||
let _ = $object.insert(($($key)+).into(), $value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next value is `null`.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: null $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(null)) $($rest)*);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next value is `true`.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: true $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(true)) $($rest)*);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next value is `false`.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: false $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!(false)) $($rest)*);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next value is an array.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: [$($array:tt)*] $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!([$($array)*])) $($rest)*);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next value is a map.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!({$($map)*})) $($rest)*);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional insert with trailing comma: key?: expr,
|
|
||||||
(@object $object:ident ($($key:tt)+) (:? $value:expr , $($rest:tt)*) $copy:tt) => {
|
|
||||||
if let Some(__v) = $crate::plist_macro::plist_maybe($value) {
|
|
||||||
let _ = $object.insert(($($key)+).into(), __v);
|
|
||||||
}
|
|
||||||
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional insert, last entry: key?: expr
|
|
||||||
(@object $object:ident ($($key:tt)+) (:? $value:expr) $copy:tt) => {
|
|
||||||
if let Some(__v) = $crate::plist_macro::plist_maybe($value) {
|
|
||||||
let _ = $object.insert(($($key)+).into(), __v);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(@object $object:ident () ( :< $value:expr , $($rest:tt)*) $copy:tt) => {
|
|
||||||
{
|
|
||||||
let __v = $crate::plist_internal!($value);
|
|
||||||
let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v);
|
|
||||||
for (__k, __val) in __dict {
|
|
||||||
let _ = $object.insert(__k, __val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge: last entry `:< expr`
|
|
||||||
(@object $object:ident () ( :< $value:expr ) $copy:tt) => {
|
|
||||||
{
|
|
||||||
let __v = $crate::plist_internal!($value);
|
|
||||||
let __dict = $crate::plist_macro::IntoPlistDict::into_plist_dict(__v);
|
|
||||||
for (__k, __val) in __dict {
|
|
||||||
let _ = $object.insert(__k, __val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional merge: `:< ? expr,` — only merge if Some(...)
|
|
||||||
(@object $object:ident () ( :< ? $value:expr , $($rest:tt)*) $copy:tt) => {
|
|
||||||
if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) {
|
|
||||||
for (__k, __val) in __dict {
|
|
||||||
let _ = $object.insert(__k, __val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$crate::plist_internal!(@object $object () ($($rest)*) ($($rest)*));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional merge: last entry `:< ? expr`
|
|
||||||
(@object $object:ident () ( :< ? $value:expr ) $copy:tt) => {
|
|
||||||
if let Some(__dict) = $crate::plist_macro::maybe_into_dict($value) {
|
|
||||||
for (__k, __val) in __dict {
|
|
||||||
let _ = $object.insert(__k, __val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Next value is an expression followed by comma.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)) , $($rest)*);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Last value is an expression with no trailing comma.
|
|
||||||
(@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object [$($key)+] ($crate::plist_internal!($value)));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Missing value for last entry. Trigger a reasonable error message.
|
|
||||||
(@object $object:ident ($($key:tt)+) (:) $copy:tt) => {
|
|
||||||
// "unexpected end of macro invocation"
|
|
||||||
$crate::plist_internal!();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Missing colon and value for last entry. Trigger a reasonable error
|
|
||||||
// message.
|
|
||||||
(@object $object:ident ($($key:tt)+) () $copy:tt) => {
|
|
||||||
// "unexpected end of macro invocation"
|
|
||||||
$crate::plist_internal!();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Misplaced colon. Trigger a reasonable error message.
|
|
||||||
(@object $object:ident () (: $($rest:tt)*) ($colon:tt $($copy:tt)*)) => {
|
|
||||||
// Takes no arguments so "no rules expected the token `:`".
|
|
||||||
$crate::plist_unexpected!($colon);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Found a comma inside a key. Trigger a reasonable error message.
|
|
||||||
(@object $object:ident ($($key:tt)*) (, $($rest:tt)*) ($comma:tt $($copy:tt)*)) => {
|
|
||||||
// Takes no arguments so "no rules expected the token `,`".
|
|
||||||
$crate::plist_unexpected!($comma);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Key is fully parenthesized. This avoids clippy double_parens false
|
|
||||||
// positives because the parenthesization may be necessary here.
|
|
||||||
(@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Refuse to absorb colon token into key expression.
|
|
||||||
(@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => {
|
|
||||||
$crate::plist_expect_expr_comma!($($unexpected)+);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Munch a token into the current key.
|
|
||||||
(@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
|
|
||||||
$crate::plist_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
|
|
||||||
};
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
// The main implementation.
|
|
||||||
//
|
|
||||||
// Must be invoked as: plist_internal!($($plist)+)
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
(null) => {
|
|
||||||
plist::Value::String("".to_string()) // plist doesn't have null, use empty string or consider other representation
|
|
||||||
};
|
|
||||||
|
|
||||||
(true) => {
|
|
||||||
plist::Value::Boolean(true)
|
|
||||||
};
|
|
||||||
|
|
||||||
(false) => {
|
|
||||||
plist::Value::Boolean(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
([]) => {
|
|
||||||
plist::Value::Array(vec![])
|
|
||||||
};
|
|
||||||
|
|
||||||
([ $($tt:tt)+ ]) => {
|
|
||||||
plist::Value::Array($crate::plist_internal!(@array [] $($tt)+))
|
|
||||||
};
|
|
||||||
|
|
||||||
({}) => {
|
|
||||||
plist::Value::Dictionary(plist::Dictionary::new())
|
|
||||||
};
|
|
||||||
|
|
||||||
({ $($tt:tt)+ }) => {
|
|
||||||
plist::Value::Dictionary({
|
|
||||||
let mut object = plist::Dictionary::new();
|
|
||||||
$crate::plist_internal!(@object object () ($($tt)+) ($($tt)+));
|
|
||||||
object
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Any type that can be converted to plist::Value: numbers, strings, variables etc.
|
|
||||||
// Must be below every other rule.
|
|
||||||
($other:expr) => {
|
|
||||||
$crate::plist_macro::plist_to_value($other)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
#[doc(hidden)]
|
|
||||||
macro_rules! plist_unexpected {
|
|
||||||
() => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
#[doc(hidden)]
|
|
||||||
macro_rules! plist_expect_expr_comma {
|
|
||||||
($e:expr , $($tt:tt)*) => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert various types to plist::Value
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn plist_to_value<T: PlistConvertible>(value: T) -> plist::Value {
|
|
||||||
value.to_plist_value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trait for types that can be converted to plist::Value
|
|
||||||
pub trait PlistConvertible {
|
|
||||||
fn to_plist_value(self) -> plist::Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementations for common types
|
|
||||||
impl PlistConvertible for plist::Value {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for String {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::String(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for &str {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::String(self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for i16 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Integer(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for i32 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Integer(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for i64 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Integer(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for u16 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Integer((self as i64).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for u32 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Integer((self as i64).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for u64 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Integer((self as i64).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for f32 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Real(self as f64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for f64 {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Real(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for bool {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Boolean(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> PlistConvertible for std::borrow::Cow<'a, str> {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::String(self.into_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PlistConvertible for Vec<u8> {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Data(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PlistConvertible for &[u8] {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Data(self.to_vec())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PlistConvertible for std::time::SystemTime {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Date(self.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PlistConvertible> PlistConvertible for Vec<T> {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PlistConvertible + Clone> PlistConvertible for &[T] {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Array(
|
|
||||||
self.iter()
|
|
||||||
.map(|item| item.clone().to_plist_value())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PlistConvertible + Clone, const N: usize> PlistConvertible for [T; N] {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Array(self.into_iter().map(|item| item.to_plist_value()).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PlistConvertible + Clone, const N: usize> PlistConvertible for &[T; N] {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Array(
|
|
||||||
self.iter()
|
|
||||||
.map(|item| item.clone().to_plist_value())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlistConvertible for plist::Dictionary {
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
plist::Value::Dictionary(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> PlistConvertible for std::collections::HashMap<K, V>
|
|
||||||
where
|
|
||||||
K: Into<String>,
|
|
||||||
V: PlistConvertible,
|
|
||||||
{
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
let mut dict = plist::Dictionary::new();
|
|
||||||
for (key, value) in self {
|
|
||||||
dict.insert(key.into(), value.to_plist_value());
|
|
||||||
}
|
|
||||||
plist::Value::Dictionary(dict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> PlistConvertible for std::collections::BTreeMap<K, V>
|
|
||||||
where
|
|
||||||
K: Into<String>,
|
|
||||||
V: PlistConvertible,
|
|
||||||
{
|
|
||||||
fn to_plist_value(self) -> plist::Value {
|
|
||||||
let mut dict = plist::Dictionary::new();
|
|
||||||
for (key, value) in self {
|
|
||||||
dict.insert(key.into(), value.to_plist_value());
|
|
||||||
}
|
|
||||||
plist::Value::Dictionary(dict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat plain T as Some(T) and Option<T> as-is.
|
|
||||||
pub trait MaybePlist {
|
|
||||||
fn into_option_value(self) -> Option<plist::Value>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PlistConvertible> MaybePlist for T {
|
|
||||||
fn into_option_value(self) -> Option<plist::Value> {
|
|
||||||
Some(self.to_plist_value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PlistConvertible> MaybePlist for Option<T> {
|
|
||||||
fn into_option_value(self) -> Option<plist::Value> {
|
|
||||||
self.map(|v| v.to_plist_value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn plist_maybe<T: MaybePlist>(v: T) -> Option<plist::Value> {
|
|
||||||
v.into_option_value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert things into a Dictionary we can merge.
|
|
||||||
pub trait IntoPlistDict {
|
|
||||||
fn into_plist_dict(self) -> plist::Dictionary;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoPlistDict for plist::Dictionary {
|
|
||||||
fn into_plist_dict(self) -> plist::Dictionary {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoPlistDict for plist::Value {
|
|
||||||
fn into_plist_dict(self) -> plist::Dictionary {
|
|
||||||
match self {
|
|
||||||
plist::Value::Dictionary(d) => d,
|
|
||||||
other => panic!("plist :< expects a dictionary, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> IntoPlistDict for std::collections::HashMap<K, V>
|
|
||||||
where
|
|
||||||
K: Into<String>,
|
|
||||||
V: PlistConvertible,
|
|
||||||
{
|
|
||||||
fn into_plist_dict(self) -> plist::Dictionary {
|
|
||||||
let mut d = plist::Dictionary::new();
|
|
||||||
for (k, v) in self {
|
|
||||||
d.insert(k.into(), v.to_plist_value());
|
|
||||||
}
|
|
||||||
d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> IntoPlistDict for std::collections::BTreeMap<K, V>
|
|
||||||
where
|
|
||||||
K: Into<String>,
|
|
||||||
V: PlistConvertible,
|
|
||||||
{
|
|
||||||
fn into_plist_dict(self) -> plist::Dictionary {
|
|
||||||
let mut d = plist::Dictionary::new();
|
|
||||||
for (k, v) in self {
|
|
||||||
d.insert(k.into(), v.to_plist_value());
|
|
||||||
}
|
|
||||||
d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional version: T or Option<T>.
|
|
||||||
pub trait MaybeIntoPlistDict {
|
|
||||||
fn into_option_plist_dict(self) -> Option<plist::Dictionary>;
|
|
||||||
}
|
|
||||||
impl<T: IntoPlistDict> MaybeIntoPlistDict for T {
|
|
||||||
fn into_option_plist_dict(self) -> Option<plist::Dictionary> {
|
|
||||||
Some(self.into_plist_dict())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T: IntoPlistDict> MaybeIntoPlistDict for Option<T> {
|
|
||||||
fn into_option_plist_dict(self) -> Option<plist::Dictionary> {
|
|
||||||
self.map(|t| t.into_plist_dict())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub fn maybe_into_dict<T: MaybeIntoPlistDict>(v: T) -> Option<plist::Dictionary> {
|
|
||||||
v.into_option_plist_dict()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#[test]
|
|
||||||
fn test_plist_macro_basic() {
|
|
||||||
let value = plist!({
|
|
||||||
"name": "test",
|
|
||||||
"count": 42,
|
|
||||||
"active": true,
|
|
||||||
"items": ["a", ?"b", "c"]
|
|
||||||
});
|
|
||||||
|
|
||||||
if let plist::Value::Dictionary(dict) = value {
|
|
||||||
assert_eq!(
|
|
||||||
dict.get("name"),
|
|
||||||
Some(&plist::Value::String("test".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(dict.get("count"), Some(&plist::Value::Integer(42.into())));
|
|
||||||
assert_eq!(dict.get("active"), Some(&plist::Value::Boolean(true)));
|
|
||||||
} else {
|
|
||||||
panic!("Expected dictionary");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_plist_macro_with_variables() {
|
|
||||||
let name = "dynamic";
|
|
||||||
let count = 100;
|
|
||||||
let items = vec!["x", "y"];
|
|
||||||
let none: Option<u64> = None;
|
|
||||||
|
|
||||||
let to_merge = plist!({
|
|
||||||
"reee": "cool beans"
|
|
||||||
});
|
|
||||||
let maybe_merge = Some(plist!({
|
|
||||||
"yeppers": "what did I say about yeppers",
|
|
||||||
"replace me": 2,
|
|
||||||
}));
|
|
||||||
let value = plist!({
|
|
||||||
"name": name,
|
|
||||||
"count": count,
|
|
||||||
"items": items,
|
|
||||||
"omit me":? none,
|
|
||||||
"keep me":? Some(123),
|
|
||||||
"replace me": 1,
|
|
||||||
:< to_merge,
|
|
||||||
:<? maybe_merge
|
|
||||||
});
|
|
||||||
|
|
||||||
if let plist::Value::Dictionary(dict) = value {
|
|
||||||
assert_eq!(
|
|
||||||
dict.get("name"),
|
|
||||||
Some(&plist::Value::String("dynamic".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(dict.get("count"), Some(&plist::Value::Integer(100.into())));
|
|
||||||
assert!(dict.get("omit me").is_none());
|
|
||||||
assert_eq!(
|
|
||||||
dict.get("keep me"),
|
|
||||||
Some(&plist::Value::Integer(123.into()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
dict.get("reee"),
|
|
||||||
Some(&plist::Value::String("cool beans".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
dict.get("yeppers"),
|
|
||||||
Some(&plist::Value::String(
|
|
||||||
"what did I say about yeppers".to_string()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
dict.get("replace me"),
|
|
||||||
Some(&plist::Value::Integer(2.into()))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("Expected dictionary");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -140,7 +140,7 @@ crate::impl_to_structs!(InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; {
|
|||||||
let mut collected_bytes = Vec::with_capacity(n);
|
let mut collected_bytes = Vec::with_capacity(n);
|
||||||
|
|
||||||
for chunk in chunk_number(n, MAX_TRANSFER as usize) {
|
for chunk in chunk_number(n, MAX_TRANSFER as usize) {
|
||||||
let header_payload = [self.fd.to_le_bytes(), chunk.to_le_bytes()].concat();
|
let header_payload = [self.fd.to_le_bytes(), (chunk as u64).to_le_bytes()].concat();
|
||||||
let res = self
|
let res = self
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.send_packet(AfcOpcode::Read, header_payload, Vec::new())
|
.send_packet(AfcOpcode::Read, header_payload, Vec::new())
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use tracing::warn;
|
|||||||
use crate::{
|
use crate::{
|
||||||
Idevice, IdeviceError, IdeviceService,
|
Idevice, IdeviceError, IdeviceService,
|
||||||
afc::file::{FileDescriptor, OwnedFileDescriptor},
|
afc::file::{FileDescriptor, OwnedFileDescriptor},
|
||||||
|
lockdown::LockdownClient,
|
||||||
obf,
|
obf,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,6 +92,43 @@ impl AfcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connects to afc2 from a provider
|
||||||
|
pub async fn new_afc2(
|
||||||
|
provider: &dyn crate::provider::IdeviceProvider,
|
||||||
|
) -> Result<Self, IdeviceError> {
|
||||||
|
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "openssl")]
|
||||||
|
let legacy = lockdown
|
||||||
|
.get_value(Some("ProductVersion"), None)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|x| x.as_string())
|
||||||
|
.and_then(|x| x.split(".").next())
|
||||||
|
.and_then(|x| x.parse::<u8>().ok())
|
||||||
|
.map(|x| x < 5)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "openssl"))]
|
||||||
|
let legacy = false;
|
||||||
|
|
||||||
|
lockdown
|
||||||
|
.start_session(&provider.get_pairing_file().await?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (port, ssl) = lockdown.start_service(obf!("com.apple.afc2")).await?;
|
||||||
|
|
||||||
|
let mut idevice = provider.connect(port).await?;
|
||||||
|
if ssl {
|
||||||
|
idevice
|
||||||
|
.start_session(&provider.get_pairing_file().await?, legacy)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::from_stream(idevice).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Lists the contents of a directory on the device
|
/// Lists the contents of a directory on the device
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use plist_macro::plist_to_xml_bytes;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
@@ -216,7 +217,7 @@ impl<R: ReadWrite> AppServiceClient<R> {
|
|||||||
"user": {
|
"user": {
|
||||||
"active": true,
|
"active": true,
|
||||||
},
|
},
|
||||||
"platformSpecificOptions": plist::Value::Data(crate::util::plist_to_xml_bytes(&platform_options.unwrap_or_default())),
|
"platformSpecificOptions": plist::Value::Data(plist_to_xml_bytes(&platform_options.unwrap_or_default())),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ impl LockdownClient {
|
|||||||
&mut self,
|
&mut self,
|
||||||
host_id: impl Into<String>,
|
host_id: impl Into<String>,
|
||||||
system_buid: impl Into<String>,
|
system_buid: impl Into<String>,
|
||||||
|
host_name: Option<&str>,
|
||||||
) -> Result<crate::pairing_file::PairingFile, IdeviceError> {
|
) -> Result<crate::pairing_file::PairingFile, IdeviceError> {
|
||||||
let host_id = host_id.into();
|
let host_id = host_id.into();
|
||||||
let system_buid = system_buid.into();
|
let system_buid = system_buid.into();
|
||||||
@@ -297,6 +298,7 @@ impl LockdownClient {
|
|||||||
let req = crate::plist!({
|
let req = crate::plist!({
|
||||||
"Label": self.idevice.label.clone(),
|
"Label": self.idevice.label.clone(),
|
||||||
"Request": "Pair",
|
"Request": "Pair",
|
||||||
|
"HostName":? host_name,
|
||||||
"PairRecord": pair_record.clone(),
|
"PairRecord": pair_record.clone(),
|
||||||
"ProtocolVersion": "2",
|
"ProtocolVersion": "2",
|
||||||
"PairingOptions": {
|
"PairingOptions": {
|
||||||
@@ -326,6 +328,23 @@ impl LockdownClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tell the device to enter recovery mode
|
||||||
|
pub async fn enter_recovery(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
self.idevice
|
||||||
|
.send_plist(crate::plist!({
|
||||||
|
"Request": "EnterRecovery"
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let res = self.idevice.read_plist().await?;
|
||||||
|
|
||||||
|
if res.get("Request").and_then(|x| x.as_string()) == Some("EnterRecovery") {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(IdeviceError::UnexpectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Idevice> for LockdownClient {
|
impl From<Idevice> for LockdownClient {
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ impl ImageMounter {
|
|||||||
self.idevice.send_plist(req).await?;
|
self.idevice.send_plist(req).await?;
|
||||||
|
|
||||||
let res = self.idevice.read_plist().await?;
|
let res = self.idevice.read_plist().await?;
|
||||||
match res.get("ImageSignature") {
|
match res
|
||||||
|
.get("ImageSignature")
|
||||||
|
.and_then(|x| x.as_array())
|
||||||
|
.and_then(|x| x.first())
|
||||||
|
{
|
||||||
Some(plist::Value::Data(signature)) => Ok(signature.clone()),
|
Some(plist::Value::Data(signature)) => Ok(signature.clone()),
|
||||||
_ => Err(IdeviceError::NotFound),
|
_ => Err(IdeviceError::NotFound),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ pub mod mobile_image_mounter;
|
|||||||
pub mod mobileactivationd;
|
pub mod mobileactivationd;
|
||||||
#[cfg(feature = "mobilebackup2")]
|
#[cfg(feature = "mobilebackup2")]
|
||||||
pub mod mobilebackup2;
|
pub mod mobilebackup2;
|
||||||
|
#[cfg(feature = "notification_proxy")]
|
||||||
|
pub mod notification_proxy;
|
||||||
#[cfg(feature = "syslog_relay")]
|
#[cfg(feature = "syslog_relay")]
|
||||||
pub mod os_trace_relay;
|
pub mod os_trace_relay;
|
||||||
#[cfg(feature = "pcapd")]
|
#[cfg(feature = "pcapd")]
|
||||||
|
|||||||
212
idevice/src/services/notification_proxy.rs
Normal file
212
idevice/src/services/notification_proxy.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
//! iOS Device Notification Proxy Service
|
||||||
|
//!
|
||||||
|
//! Based on libimobiledevice's notification_proxy implementation
|
||||||
|
//!
|
||||||
|
//! Common notification identifiers:
|
||||||
|
//! Full list: include/libimobiledevice/notification_proxy.h
|
||||||
|
//!
|
||||||
|
//! - Notifications that can be sent (PostNotification):
|
||||||
|
//! - `com.apple.itunes-mobdev.syncWillStart` - Sync will start
|
||||||
|
//! - `com.apple.itunes-mobdev.syncDidStart` - Sync started
|
||||||
|
//! - `com.apple.itunes-mobdev.syncDidFinish` - Sync finished
|
||||||
|
//! - `com.apple.itunes-mobdev.syncLockRequest` - Request sync lock
|
||||||
|
//!
|
||||||
|
//! - Notifications that can be observed (ObserveNotification):
|
||||||
|
//! - `com.apple.itunes-client.syncCancelRequest` - Cancel sync request
|
||||||
|
//! - `com.apple.itunes-client.syncSuspendRequest` - Suspend sync
|
||||||
|
//! - `com.apple.itunes-client.syncResumeRequest` - Resume sync
|
||||||
|
//! - `com.apple.mobile.lockdown.phone_number_changed` - Phone number changed
|
||||||
|
//! - `com.apple.mobile.lockdown.device_name_changed` - Device name changed
|
||||||
|
//! - `com.apple.mobile.lockdown.timezone_changed` - Timezone changed
|
||||||
|
//! - `com.apple.mobile.lockdown.trusted_host_attached` - Trusted host attached
|
||||||
|
//! - `com.apple.mobile.lockdown.host_detached` - Host detached
|
||||||
|
//! - `com.apple.mobile.lockdown.host_attached` - Host attached
|
||||||
|
//! - `com.apple.mobile.lockdown.registration_failed` - Registration failed
|
||||||
|
//! - `com.apple.mobile.lockdown.activation_state` - Activation state
|
||||||
|
//! - `com.apple.mobile.lockdown.brick_state` - Brick state
|
||||||
|
//! - `com.apple.mobile.lockdown.disk_usage_changed` - Disk usage (iOS 4.0+)
|
||||||
|
//! - `com.apple.mobile.data_sync.domain_changed` - Data sync domain changed
|
||||||
|
//! - `com.apple.mobile.application_installed` - App installed
|
||||||
|
//! - `com.apple.mobile.application_uninstalled` - App uninstalled
|
||||||
|
|
||||||
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use futures::Stream;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{Idevice, IdeviceError, IdeviceService, obf};
|
||||||
|
|
||||||
|
/// Client for interacting with the iOS notification proxy service
|
||||||
|
///
|
||||||
|
/// The notification proxy service provides a mechanism to observe and post
|
||||||
|
/// system notifications.
|
||||||
|
///
|
||||||
|
/// Use `observe_notification` to register for events, then `receive_notification`
|
||||||
|
/// to wait for them.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NotificationProxyClient {
|
||||||
|
/// The underlying device connection with established notification_proxy service
|
||||||
|
pub idevice: Idevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdeviceService for NotificationProxyClient {
|
||||||
|
/// Returns the notification proxy service name as registered with lockdownd
|
||||||
|
fn service_name() -> std::borrow::Cow<'static, str> {
|
||||||
|
obf!("com.apple.mobile.notification_proxy")
|
||||||
|
}
|
||||||
|
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
|
||||||
|
Ok(Self::new(idevice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotificationProxyClient {
|
||||||
|
/// Creates a new notification proxy client from an existing device connection
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `idevice` - Pre-established device connection
|
||||||
|
pub fn new(idevice: Idevice) -> Self {
|
||||||
|
Self { idevice }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posts a notification to the device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `notification_name` - Name of the notification to post
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if the notification fails to send
|
||||||
|
pub async fn post_notification(
|
||||||
|
&mut self,
|
||||||
|
notification_name: impl Into<String>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
let request = crate::plist!({
|
||||||
|
"Command": "PostNotification",
|
||||||
|
"Name": notification_name.into()
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers to observe a specific notification
|
||||||
|
///
|
||||||
|
/// After calling this, use `receive_notification` to wait for events.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `notification_name` - Name of the notification to observe
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if the registration fails
|
||||||
|
pub async fn observe_notification(
|
||||||
|
&mut self,
|
||||||
|
notification_name: impl Into<String>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
let request = crate::plist!({
|
||||||
|
"Command": "ObserveNotification",
|
||||||
|
"Name": notification_name.into()
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers to observe multiple notifications at once
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `notification_names` - Slice of notification names to observe
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if any registration fails
|
||||||
|
pub async fn observe_notifications(
|
||||||
|
&mut self,
|
||||||
|
notification_names: &[&str],
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
for name in notification_names {
|
||||||
|
self.observe_notification(*name).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for and receives the next notification from the device
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The name of the received notification
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// - `NotificationProxyDeath` if the proxy connection died
|
||||||
|
/// - `UnexpectedResponse` if the response format is invalid
|
||||||
|
pub async fn receive_notification(&mut self) -> Result<String, IdeviceError> {
|
||||||
|
let response = self.idevice.read_plist().await?;
|
||||||
|
|
||||||
|
match response.get("Command").and_then(|c| c.as_string()) {
|
||||||
|
Some("RelayNotification") => match response.get("Name").and_then(|n| n.as_string()) {
|
||||||
|
Some(name) => Ok(name.to_string()),
|
||||||
|
None => Err(IdeviceError::UnexpectedResponse),
|
||||||
|
},
|
||||||
|
Some("ProxyDeath") => {
|
||||||
|
warn!("NotificationProxy died!");
|
||||||
|
Err(IdeviceError::NotificationProxyDeath)
|
||||||
|
}
|
||||||
|
_ => Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for a notification with a timeout
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `interval` - Timeout in seconds to wait for a notification
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The name of the received notification
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// - `NotificationProxyDeath` if the proxy connection died
|
||||||
|
/// - `UnexpectedResponse` if the response format is invalid
|
||||||
|
/// - `HeartbeatTimeout` if no notification received before interval
|
||||||
|
pub async fn receive_notification_with_timeout(
|
||||||
|
&mut self,
|
||||||
|
interval: u64,
|
||||||
|
) -> Result<String, IdeviceError> {
|
||||||
|
tokio::select! {
|
||||||
|
result = self.receive_notification() => result,
|
||||||
|
_ = tokio::time::sleep(tokio::time::Duration::from_secs(interval)) => {
|
||||||
|
Err(IdeviceError::HeartbeatTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continuous stream of notifications.
|
||||||
|
pub fn into_stream(
|
||||||
|
mut self,
|
||||||
|
) -> Pin<Box<dyn Stream<Item = Result<String, IdeviceError>> + Send>> {
|
||||||
|
Box::pin(async_stream::try_stream! {
|
||||||
|
loop {
|
||||||
|
let response = self.idevice.read_plist().await?;
|
||||||
|
|
||||||
|
match response.get("Command").and_then(|c| c.as_string()) {
|
||||||
|
Some("RelayNotification") => {
|
||||||
|
match response.get("Name").and_then(|n| n.as_string()) {
|
||||||
|
Some(name) => yield name.to_string(),
|
||||||
|
None => Err(IdeviceError::UnexpectedResponse)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("ProxyDeath") => {
|
||||||
|
warn!("NotificationProxy died!");
|
||||||
|
Err(IdeviceError::NotificationProxyDeath)?;
|
||||||
|
}
|
||||||
|
_ => Err(IdeviceError::UnexpectedResponse)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shuts down the notification proxy connection
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if the shutdown command fails to send
|
||||||
|
pub async fn shutdown(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
let request = crate::plist!({
|
||||||
|
"Command": "Shutdown"
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(request).await?;
|
||||||
|
// Best-effort: wait for ProxyDeath ack
|
||||||
|
let _ = self.idevice.read_plist().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,23 @@
|
|||||||
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
|
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
|
||||||
//! which manages home screen and app icon related operations.
|
//! which manages home screen and app icon related operations.
|
||||||
|
|
||||||
use crate::{Idevice, IdeviceError, IdeviceService, obf};
|
use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds};
|
||||||
|
|
||||||
|
/// Orientation of the device
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum InterfaceOrientation {
|
||||||
|
/// Orientation is unknown or cannot be determined
|
||||||
|
Unknown = 0,
|
||||||
|
/// Portrait mode (normal vertical)
|
||||||
|
Portrait = 1,
|
||||||
|
/// Portrait mode upside down
|
||||||
|
PortraitUpsideDown = 2,
|
||||||
|
/// Landscape with home button on the right (notch to the left)
|
||||||
|
LandscapeRight = 3,
|
||||||
|
/// Landscape with home button on the left (notch to the right)
|
||||||
|
LandscapeLeft = 4,
|
||||||
|
}
|
||||||
|
|
||||||
/// Client for interacting with the iOS SpringBoard services
|
/// Client for interacting with the iOS SpringBoard services
|
||||||
///
|
///
|
||||||
@@ -70,4 +86,270 @@ impl SpringBoardServicesClient {
|
|||||||
_ => Err(IdeviceError::UnexpectedResponse),
|
_ => Err(IdeviceError::UnexpectedResponse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the current icon state from the device
|
||||||
|
///
|
||||||
|
/// The icon state contains the layout and organization of all apps on the home screen,
|
||||||
|
/// including folder structures and icon positions. This is a read-only operation.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `format_version` - Optional format version string for the icon state format
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A plist Value containing the complete icon state structure
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The response is malformed
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use idevice::services::springboardservices::SpringBoardServicesClient;
|
||||||
|
///
|
||||||
|
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
|
||||||
|
/// let icon_state = client.get_icon_state(None).await?;
|
||||||
|
/// println!("Icon state: {:?}", icon_state);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
/// This method successfully reads the home screen layout on all iOS versions.
|
||||||
|
pub async fn get_icon_state(
|
||||||
|
&mut self,
|
||||||
|
format_version: Option<&str>,
|
||||||
|
) -> Result<plist::Value, IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "getIconState",
|
||||||
|
"formatVersion":? format_version,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
let mut res = self.idevice.read_plist_value().await?;
|
||||||
|
|
||||||
|
// Some devices may return an error dictionary instead of icon state.
|
||||||
|
// Detect this and surface it as an UnexpectedResponse, similar to get_icon_pngdata.
|
||||||
|
if let plist::Value::Dictionary(ref dict) = res
|
||||||
|
&& (dict.contains_key("error") || dict.contains_key("Error"))
|
||||||
|
{
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut res);
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the icon state on the device
|
||||||
|
///
|
||||||
|
/// This method allows you to modify the home screen layout by providing a new icon state.
|
||||||
|
/// The icon state structure should match the format returned by `get_icon_state`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `icon_state` - A plist Value containing the complete icon state structure
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Ok(()) if the icon state was successfully set
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The icon state format is invalid
|
||||||
|
/// - The device rejects the new layout
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use idevice::services::springboardservices::SpringBoardServicesClient;
|
||||||
|
///
|
||||||
|
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
|
||||||
|
/// let mut icon_state = client.get_icon_state(None).await?;
|
||||||
|
///
|
||||||
|
/// // Modify the icon state (e.g., swap two icons)
|
||||||
|
/// // ... modify icon_state ...
|
||||||
|
///
|
||||||
|
/// client.set_icon_state(icon_state).await?;
|
||||||
|
/// println!("Icon state updated successfully");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
/// - Changes take effect immediately
|
||||||
|
/// - The device may validate the icon state structure before applying
|
||||||
|
/// - Invalid icon states will be rejected by the device
|
||||||
|
pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "setIconState",
|
||||||
|
"iconState": icon_state,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the icon state with a specific format version
|
||||||
|
///
|
||||||
|
/// This is similar to `set_icon_state` but allows specifying a format version.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `icon_state` - A plist Value containing the complete icon state structure
|
||||||
|
/// * `format_version` - Optional format version string
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Ok(()) if the icon state was successfully set
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The icon state format is invalid
|
||||||
|
/// - The device rejects the new layout
|
||||||
|
pub async fn set_icon_state_with_version(
|
||||||
|
&mut self,
|
||||||
|
icon_state: plist::Value,
|
||||||
|
format_version: Option<&str>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "setIconState",
|
||||||
|
"iconState": icon_state,
|
||||||
|
"formatVersion":? format_version,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the home screen wallpaper preview as PNG data
|
||||||
|
///
|
||||||
|
/// This gets a rendered preview of the home screen wallpaper.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The raw PNG data of the home screen wallpaper preview
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The device rejects the request
|
||||||
|
/// - The image is malformed/corupted
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// let wallpaper = client.get_home_screen_wallpaper_preview_pngdata().await?;
|
||||||
|
/// std::fs::write("home.png", wallpaper)?;
|
||||||
|
/// ```
|
||||||
|
pub async fn get_home_screen_wallpaper_preview_pngdata(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<u8>, IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "getWallpaperPreviewImage",
|
||||||
|
"wallpaperName": "homescreen",
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
|
||||||
|
let mut res = self.idevice.read_plist().await?;
|
||||||
|
match res.remove("pngData") {
|
||||||
|
Some(plist::Value::Data(res)) => Ok(res),
|
||||||
|
_ => Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the lock screen wallpaper preview as PNG data
|
||||||
|
///
|
||||||
|
/// This gets a rendered preview of the lock screen wallpaper.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The raw PNG data of the lock screen wallpaper preview
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The device rejects the request
|
||||||
|
/// - The image is malformed/corupted
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// let wallpaper = client.get_lock_screen_wallpaper_preview_pngdata().await?;
|
||||||
|
/// std::fs::write("lock.png", wallpaper)?;
|
||||||
|
/// ```
|
||||||
|
pub async fn get_lock_screen_wallpaper_preview_pngdata(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<u8>, IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "getWallpaperPreviewImage",
|
||||||
|
"wallpaperName": "lockscreen",
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
|
||||||
|
let mut res = self.idevice.read_plist().await?;
|
||||||
|
match res.remove("pngData") {
|
||||||
|
Some(plist::Value::Data(res)) => Ok(res),
|
||||||
|
_ => Err(IdeviceError::UnexpectedResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current interface orientation of the device
|
||||||
|
///
|
||||||
|
/// This gets which way the device is currently facing
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The current `InterfaceOrientation` of the device
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The device doesn't support this command
|
||||||
|
/// - The response format is unexpected
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// let orientation = client.get_interface_orientation().await?;
|
||||||
|
/// println!("Device orientation: {:?}", orientation);
|
||||||
|
/// ```
|
||||||
|
pub async fn get_interface_orientation(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<InterfaceOrientation, IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "getInterfaceOrientation",
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
|
||||||
|
let res = self.idevice.read_plist().await?;
|
||||||
|
let orientation_value = res
|
||||||
|
.get("interfaceOrientation")
|
||||||
|
.and_then(|v| v.as_unsigned_integer())
|
||||||
|
.ok_or(IdeviceError::UnexpectedResponse)?;
|
||||||
|
|
||||||
|
let orientation = match orientation_value {
|
||||||
|
1 => InterfaceOrientation::Portrait,
|
||||||
|
2 => InterfaceOrientation::PortraitUpsideDown,
|
||||||
|
3 => InterfaceOrientation::LandscapeRight,
|
||||||
|
4 => InterfaceOrientation::LandscapeLeft,
|
||||||
|
_ => InterfaceOrientation::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the home screen icon layout metrics
|
||||||
|
///
|
||||||
|
/// Returns icon spacing, size, and positioning information
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `plist::Dictionary` containing the icon layout metrics
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The response is malformed
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// let metrics = client.get_homescreen_icon_metrics().await?;
|
||||||
|
/// println!("{:?}", metrics);
|
||||||
|
/// ```
|
||||||
|
pub async fn get_homescreen_icon_metrics(&mut self) -> Result<plist::Dictionary, IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "getHomeScreenIconMetrics",
|
||||||
|
});
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
|
||||||
|
let res = self.idevice.read_plist().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
//! - Handle cryptographic signing operations
|
//! - Handle cryptographic signing operations
|
||||||
|
|
||||||
use plist::Value;
|
use plist::Value;
|
||||||
|
use plist_macro::plist_to_xml_bytes;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{IdeviceError, util::plist_to_xml_bytes};
|
use crate::IdeviceError;
|
||||||
|
|
||||||
/// TSS client version string sent in requests
|
/// TSS client version string sent in requests
|
||||||
const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2";
|
const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2";
|
||||||
@@ -30,7 +31,7 @@ impl TSSRequest {
|
|||||||
/// - Client version string
|
/// - Client version string
|
||||||
/// - Random UUID for request identification
|
/// - Random UUID for request identification
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let inner = crate::plist!(dict {
|
let inner = plist_macro::plist!(dict {
|
||||||
"@HostPlatformInfo": "mac",
|
"@HostPlatformInfo": "mac",
|
||||||
"@VersionInfo": TSS_CLIENT_VERSION_STRING,
|
"@VersionInfo": TSS_CLIENT_VERSION_STRING,
|
||||||
"@UUID": uuid::Uuid::new_v4().to_string().to_uppercase()
|
"@UUID": uuid::Uuid::new_v4().to_string().to_uppercase()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use crate::util::plist_to_xml_bytes;
|
use plist_macro::plist_to_xml_bytes;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
//! Utility Functions
|
|
||||||
//!
|
|
||||||
//! Provides helper functions for working with Apple's Property List (PLIST) format,
|
|
||||||
//! including serialization and pretty-printing utilities.
|
|
||||||
#![allow(dead_code)] // functions might not be used by all features
|
|
||||||
|
|
||||||
use plist::Value;
|
|
||||||
|
|
||||||
/// Converts a PLIST dictionary to XML-formatted bytes
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `p` - The PLIST dictionary to serialize
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A byte vector containing the XML representation
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// Will panic if serialization fails (should only happen with invalid data)
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// let mut dict = plist::Dictionary::new();
|
|
||||||
/// dict.insert("key".into(), "value".into());
|
|
||||||
/// let xml_bytes = plist_to_xml_bytes(&dict);
|
|
||||||
/// ```
|
|
||||||
pub fn plist_to_xml_bytes(p: &plist::Dictionary) -> Vec<u8> {
|
|
||||||
let buf = Vec::new();
|
|
||||||
let mut writer = std::io::BufWriter::new(buf);
|
|
||||||
plist::to_writer_xml(&mut writer, &p).unwrap();
|
|
||||||
|
|
||||||
writer.into_inner().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pretty-prints a PLIST value with indentation
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `p` - The PLIST value to format
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A formatted string representation
|
|
||||||
pub fn pretty_print_plist(p: &Value) -> String {
|
|
||||||
print_plist(p, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pretty-prints a PLIST dictionary with key-value pairs
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `dict` - The dictionary to format
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A formatted string representation with newlines and indentation
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// let mut dict = plist::Dictionary::new();
|
|
||||||
/// dict.insert("name".into(), "John".into());
|
|
||||||
/// dict.insert("age".into(), 30.into());
|
|
||||||
/// println!("{}", pretty_print_dictionary(&dict));
|
|
||||||
/// ```
|
|
||||||
pub fn pretty_print_dictionary(dict: &plist::Dictionary) -> String {
|
|
||||||
let items: Vec<String> = dict
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{}: {}", k, print_plist(v, 2)))
|
|
||||||
.collect();
|
|
||||||
format!("{{\n{}\n}}", items.join(",\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal recursive function for printing PLIST values with indentation
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `p` - The PLIST value to format
|
|
||||||
/// * `indentation` - Current indentation level
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// Formatted string representation
|
|
||||||
fn print_plist(p: &Value, indentation: usize) -> String {
|
|
||||||
let indent = " ".repeat(indentation);
|
|
||||||
match p {
|
|
||||||
Value::Array(vec) => {
|
|
||||||
let items: Vec<String> = vec
|
|
||||||
.iter()
|
|
||||||
.map(|v| {
|
|
||||||
format!(
|
|
||||||
"{}{}",
|
|
||||||
" ".repeat(indentation + 2),
|
|
||||||
print_plist(v, indentation + 2)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
format!("[\n{}\n{}]", items.join(",\n"), indent)
|
|
||||||
}
|
|
||||||
Value::Dictionary(dict) => {
|
|
||||||
let items: Vec<String> = dict
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| {
|
|
||||||
format!(
|
|
||||||
"{}{}: {}",
|
|
||||||
" ".repeat(indentation + 2),
|
|
||||||
k,
|
|
||||||
print_plist(v, indentation + 2)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
format!("{{\n{}\n{}}}", items.join(",\n"), indent)
|
|
||||||
}
|
|
||||||
Value::Boolean(b) => format!("{b}"),
|
|
||||||
Value::Data(vec) => {
|
|
||||||
let len = vec.len();
|
|
||||||
let preview: String = vec
|
|
||||||
.iter()
|
|
||||||
.take(20)
|
|
||||||
.map(|b| format!("{b:02X}"))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" ");
|
|
||||||
if len > 20 {
|
|
||||||
format!("Data({preview}... Len: {len})")
|
|
||||||
} else {
|
|
||||||
format!("Data({preview} Len: {len})")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Date(date) => format!("Date({})", date.to_xml_format()),
|
|
||||||
Value::Real(f) => format!("{f}"),
|
|
||||||
Value::Integer(i) => format!("{i}"),
|
|
||||||
Value::String(s) => format!("\"{s}\""),
|
|
||||||
Value::Uid(_uid) => "Uid(?)".to_string(),
|
|
||||||
_ => "Unknown".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! obf {
|
|
||||||
($lit:literal) => {{
|
|
||||||
#[cfg(feature = "obfuscate")]
|
|
||||||
{
|
|
||||||
std::borrow::Cow::Owned(obfstr::obfstr!($lit).to_string())
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "obfuscate"))]
|
|
||||||
{
|
|
||||||
std::borrow::Cow::Borrowed($lit)
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
use std::{io::Cursor, path::Path};
|
|
||||||
|
|
||||||
use async_zip::base::read::seek::ZipFileReader;
|
use async_zip::base::read::seek::ZipFileReader;
|
||||||
use futures::AsyncReadExt as _;
|
use futures::AsyncReadExt as _;
|
||||||
|
use plist_macro::plist;
|
||||||
|
use std::{io::Cursor, path::Path};
|
||||||
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
|
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
IdeviceError, IdeviceService,
|
IdeviceError, IdeviceService,
|
||||||
afc::{AfcClient, opcode::AfcFopenMode},
|
afc::{AfcClient, opcode::AfcFopenMode},
|
||||||
plist,
|
|
||||||
provider::IdeviceProvider,
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
|
|
||||||
#[cfg(all(feature = "afc", feature = "installation_proxy"))]
|
#[cfg(all(feature = "afc", feature = "installation_proxy"))]
|
||||||
pub mod installation;
|
pub mod installation;
|
||||||
|
|
||||||
|
pub mod plist;
|
||||||
|
|||||||
155
idevice/src/utils/plist.rs
Normal file
155
idevice/src/utils/plist.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/// Utilities for working with plist values
|
||||||
|
///
|
||||||
|
/// Truncates all Date values in a plist structure to second precision.
|
||||||
|
///
|
||||||
|
/// This function recursively walks through a plist Value and truncates any Date values
|
||||||
|
/// from nanosecond precision to second precision. This is necessary for compatibility
|
||||||
|
/// with iOS devices that reject high-precision date formats.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `value` - The plist Value to normalize (modified in place)
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use idevice::utils::plist::truncate_dates_to_seconds;
|
||||||
|
/// use plist::Value;
|
||||||
|
///
|
||||||
|
/// let mut icon_state = Value::Array(vec![]);
|
||||||
|
/// truncate_dates_to_seconds(&mut icon_state);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Details
|
||||||
|
/// - Converts dates from format: `2026-01-17T03:09:58.332738876Z` (nanosecond precision)
|
||||||
|
/// - To format: `2026-01-17T03:09:58Z` (second precision)
|
||||||
|
/// - Recursively processes Arrays and Dictionaries
|
||||||
|
/// - Other value types are left unchanged
|
||||||
|
pub fn truncate_dates_to_seconds(value: &mut plist::Value) {
|
||||||
|
match value {
|
||||||
|
plist::Value::Date(date) => {
|
||||||
|
let xml_string = date.to_xml_format();
|
||||||
|
if let Some(dot_pos) = xml_string.find('.')
|
||||||
|
&& xml_string[dot_pos..].contains('Z')
|
||||||
|
{
|
||||||
|
let truncated_string = format!("{}Z", &xml_string[..dot_pos]);
|
||||||
|
if let Ok(new_date) = plist::Date::from_xml_format(&truncated_string) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plist::Value::Array(arr) => {
|
||||||
|
for item in arr.iter_mut() {
|
||||||
|
truncate_dates_to_seconds(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plist::Value::Dictionary(dict) => {
|
||||||
|
for (_, v) in dict.iter_mut() {
|
||||||
|
truncate_dates_to_seconds(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_date_with_nanoseconds() {
|
||||||
|
let date_str = "2026-01-17T03:09:58.332738876Z";
|
||||||
|
let date = plist::Date::from_xml_format(date_str).unwrap();
|
||||||
|
let mut value = plist::Value::Date(date);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Date(truncated_date) = value {
|
||||||
|
let result = truncated_date.to_xml_format();
|
||||||
|
assert!(
|
||||||
|
!result.contains('.'),
|
||||||
|
"Date should not contain fractional seconds"
|
||||||
|
);
|
||||||
|
assert!(result.ends_with('Z'), "Date should end with Z");
|
||||||
|
assert!(
|
||||||
|
result.starts_with("2026-01-17T03:09:58"),
|
||||||
|
"Date should preserve main timestamp"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Value should still be a Date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_date_already_truncated() {
|
||||||
|
let date_str = "2026-01-17T03:09:58Z";
|
||||||
|
let date = plist::Date::from_xml_format(date_str).unwrap();
|
||||||
|
let original_format = date.to_xml_format();
|
||||||
|
let mut value = plist::Value::Date(date);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Date(truncated_date) = value {
|
||||||
|
let result = truncated_date.to_xml_format();
|
||||||
|
assert_eq!(
|
||||||
|
result, original_format,
|
||||||
|
"Already truncated date should remain unchanged"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_dates_in_array() {
|
||||||
|
let date1 = plist::Date::from_xml_format("2026-01-17T03:09:58.123456Z").unwrap();
|
||||||
|
let date2 = plist::Date::from_xml_format("2026-01-18T04:10:59.987654Z").unwrap();
|
||||||
|
let mut value =
|
||||||
|
plist::Value::Array(vec![plist::Value::Date(date1), plist::Value::Date(date2)]);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Array(arr) = value {
|
||||||
|
for item in arr {
|
||||||
|
if let plist::Value::Date(date) = item {
|
||||||
|
let formatted = date.to_xml_format();
|
||||||
|
assert!(
|
||||||
|
!formatted.contains('.'),
|
||||||
|
"Dates in array should be truncated"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_dates_in_dictionary() {
|
||||||
|
let date = plist::Date::from_xml_format("2026-01-17T03:09:58.999999Z").unwrap();
|
||||||
|
let mut dict = plist::Dictionary::new();
|
||||||
|
dict.insert("timestamp".to_string(), plist::Value::Date(date));
|
||||||
|
let mut value = plist::Value::Dictionary(dict);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Dictionary(dict) = value
|
||||||
|
&& let Some(plist::Value::Date(date)) = dict.get("timestamp")
|
||||||
|
{
|
||||||
|
let formatted = date.to_xml_format();
|
||||||
|
assert!(
|
||||||
|
!formatted.contains('.'),
|
||||||
|
"Date in dictionary should be truncated"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_other_value_types_unchanged() {
|
||||||
|
let mut string_val = plist::Value::String("test".to_string());
|
||||||
|
let mut int_val = plist::Value::Integer(42.into());
|
||||||
|
let mut bool_val = plist::Value::Boolean(true);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut string_val);
|
||||||
|
truncate_dates_to_seconds(&mut int_val);
|
||||||
|
truncate_dates_to_seconds(&mut bool_val);
|
||||||
|
|
||||||
|
assert!(matches!(string_val, plist::Value::String(_)));
|
||||||
|
assert!(matches!(int_val, plist::Value::Integer(_)));
|
||||||
|
assert!(matches!(bool_val, plist::Value::Boolean(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use plist_macro::plist;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::CString,
|
ffi::CString,
|
||||||
io::{BufRead, Cursor, Read},
|
io::{BufRead, Cursor, Read},
|
||||||
@@ -169,7 +170,7 @@ impl XPCObject {
|
|||||||
plist::Value::Dictionary(dict)
|
plist::Value::Dictionary(dict)
|
||||||
}
|
}
|
||||||
Self::FileTransfer { msg_id, data } => {
|
Self::FileTransfer { msg_id, data } => {
|
||||||
crate::plist!({
|
plist!({
|
||||||
"msg_id": *msg_id,
|
"msg_id": *msg_id,
|
||||||
"data": data.to_plist(),
|
"data": data.to_plist(),
|
||||||
})
|
})
|
||||||
|
|||||||
18
justfile
18
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
|
macos-ci-check: ci-check xcframework
|
||||||
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
|
||||||
|
|
||||||
@@ -40,6 +40,9 @@ xcframework: apple-build
|
|||||||
lipo -create -output swift/libs/idevice-ios-sim.a \
|
lipo -create -output swift/libs/idevice-ios-sim.a \
|
||||||
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
|
target/aarch64-apple-ios-sim/release/libidevice_ffi.a \
|
||||||
target/x86_64-apple-ios/release/libidevice_ffi.a
|
target/x86_64-apple-ios/release/libidevice_ffi.a
|
||||||
|
lipo -create -output swift/libs/idevice-maccatalyst.a \
|
||||||
|
target/aarch64-apple-ios-macabi/release/libidevice_ffi.a \
|
||||||
|
target/x86_64-apple-ios-macabi/release/libidevice_ffi.a
|
||||||
lipo -create -output swift/libs/idevice-macos.a \
|
lipo -create -output swift/libs/idevice-macos.a \
|
||||||
target/aarch64-apple-darwin/release/libidevice_ffi.a \
|
target/aarch64-apple-darwin/release/libidevice_ffi.a \
|
||||||
target/x86_64-apple-darwin/release/libidevice_ffi.a
|
target/x86_64-apple-darwin/release/libidevice_ffi.a
|
||||||
@@ -48,6 +51,7 @@ xcframework: apple-build
|
|||||||
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
|
-library target/aarch64-apple-ios/release/libidevice_ffi.a -headers swift/include \
|
||||||
-library swift/libs/idevice-ios-sim.a -headers swift/include \
|
-library swift/libs/idevice-ios-sim.a -headers swift/include \
|
||||||
-library swift/libs/idevice-macos.a -headers swift/include \
|
-library swift/libs/idevice-macos.a -headers swift/include \
|
||||||
|
-library swift/libs/idevice-maccatalyst.a -headers swift/include \
|
||||||
-output swift/IDevice.xcframework
|
-output swift/IDevice.xcframework
|
||||||
|
|
||||||
zip -r swift/bundle.zip swift/IDevice.xcframework
|
zip -r swift/bundle.zip swift/IDevice.xcframework
|
||||||
@@ -57,6 +61,7 @@ xcframework: apple-build
|
|||||||
apple-build: # requires a Mac
|
apple-build: # requires a Mac
|
||||||
# iOS device build
|
# iOS device build
|
||||||
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphoneos --show-sdk-path)" \
|
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphoneos --show-sdk-path)" \
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET=17.0 \
|
||||||
cargo build --release --target aarch64-apple-ios --features obfuscate
|
cargo build --release --target aarch64-apple-ios --features obfuscate
|
||||||
|
|
||||||
# iOS Simulator (arm64)
|
# iOS Simulator (arm64)
|
||||||
@@ -67,7 +72,16 @@ apple-build: # requires a Mac
|
|||||||
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \
|
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk iphonesimulator --show-sdk-path)" \
|
||||||
cargo build --release --target x86_64-apple-ios
|
cargo build --release --target x86_64-apple-ios
|
||||||
|
|
||||||
|
# Mac Catalyst (arm64)
|
||||||
|
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
|
||||||
|
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
|
||||||
|
cargo build --release --target aarch64-apple-ios-macabi --no-default-features --features "ring full"
|
||||||
|
|
||||||
|
# Mac Catalyst (x86_64)
|
||||||
|
# AWS-LC has an a hard time compiling for an iOS with macabi target, so we switch to ring.
|
||||||
|
BINDGEN_EXTRA_CLANG_ARGS="--sysroot=$(xcrun --sdk macosx --show-sdk-path)" \
|
||||||
|
cargo build --release --target x86_64-apple-ios-macabi --no-default-features --features "ring full"
|
||||||
|
|
||||||
# macOS (native) – no special env needed
|
# macOS (native) – no special env needed
|
||||||
cargo build --release --target aarch64-apple-darwin
|
cargo build --release --target aarch64-apple-darwin
|
||||||
cargo build --release --target x86_64-apple-darwin
|
cargo build --release --target x86_64-apple-darwin
|
||||||
|
|
||||||
|
|||||||
151
tools/Cargo.toml
151
tools/Cargo.toml
@@ -2,80 +2,13 @@
|
|||||||
name = "idevice-tools"
|
name = "idevice-tools"
|
||||||
description = "Rust binary tools to interact with services on iOS devices."
|
description = "Rust binary tools to interact with services on iOS devices."
|
||||||
authors = ["Jackson Coxson"]
|
authors = ["Jackson Coxson"]
|
||||||
version = "0.1.0"
|
version = "0.1.53"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
documentation = "https://docs.rs/idevice"
|
documentation = "https://docs.rs/idevice"
|
||||||
repository = "https://github.com/jkcoxson/idevice"
|
repository = "https://github.com/jkcoxson/idevice"
|
||||||
keywords = ["lockdownd", "ios"]
|
keywords = ["lockdownd", "ios"]
|
||||||
|
default-run = "idevice-tools"
|
||||||
# [[bin]]
|
|
||||||
# name = "ideviceinfo"
|
|
||||||
# path = "src/ideviceinfo.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "heartbeat_client"
|
|
||||||
# path = "src/heartbeat_client.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "instproxy"
|
|
||||||
# path = "src/instproxy.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "ideviceinstaller"
|
|
||||||
# path = "src/ideviceinstaller.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "mounter"
|
|
||||||
# path = "src/mounter.rs"
|
|
||||||
|
|
||||||
# [[bin]]
|
|
||||||
# name = "core_device_proxy_tun"
|
|
||||||
# path = "src/core_device_proxy_tun.rs"
|
|
||||||
|
|
||||||
# [[bin]]
|
|
||||||
# name = "idevice_id"
|
|
||||||
# path = "src/idevice_id.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "process_control"
|
|
||||||
# path = "src/process_control.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "dvt_packet_parser"
|
|
||||||
# path = "src/dvt_packet_parser.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "remotexpc"
|
|
||||||
# path = "src/remotexpc.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "debug_proxy"
|
|
||||||
# path = "src/debug_proxy.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "misagent"
|
|
||||||
# path = "src/misagent.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "location_simulation"
|
|
||||||
# path = "src/location_simulation.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "afc"
|
|
||||||
# path = "src/afc.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "crash_logs"
|
|
||||||
# path = "src/crash_logs.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "amfi"
|
|
||||||
# path = "src/amfi.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "pair"
|
|
||||||
# path = "src/pair.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "pair_apple_tv"
|
name = "pair_apple_tv"
|
||||||
@@ -86,74 +19,16 @@ name = "pair_rsd_ios"
|
|||||||
path = "src/pair_rsd_ios.rs"
|
path = "src/pair_rsd_ios.rs"
|
||||||
|
|
||||||
# [[bin]]
|
# [[bin]]
|
||||||
# name = "syslog_relay"
|
# name = "core_device_proxy_tun"
|
||||||
# path = "src/syslog_relay.rs"
|
# path = "src/core_device_proxy_tun.rs"
|
||||||
#
|
|
||||||
# [[bin]]
|
[[bin]]
|
||||||
# name = "os_trace_relay"
|
name = "idevice_id"
|
||||||
# path = "src/os_trace_relay.rs"
|
path = "src/idevice_id.rs"
|
||||||
#
|
|
||||||
# [[bin]]
|
[[bin]]
|
||||||
# name = "app_service"
|
name = "iproxy"
|
||||||
# path = "src/app_service.rs"
|
path = "src/iproxy.rs"
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "lockdown"
|
|
||||||
# path = "src/lockdown.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "restore_service"
|
|
||||||
# path = "src/restore_service.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "companion_proxy"
|
|
||||||
# path = "src/companion_proxy.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "diagnostics"
|
|
||||||
# path = "src/diagnostics.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "mobilebackup2"
|
|
||||||
# path = "src/mobilebackup2.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "diagnosticsservice"
|
|
||||||
# path = "src/diagnosticsservice.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "bt_packet_logger"
|
|
||||||
# path = "src/bt_packet_logger.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "pcapd"
|
|
||||||
# path = "src/pcapd.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "preboard"
|
|
||||||
# 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"
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "installcoordination_proxy"
|
|
||||||
# path = "src/installcoordination_proxy.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "iproxy"
|
|
||||||
# path = "src/iproxy.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
||||||
@@ -164,7 +39,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
sha2 = { version = "0.10" }
|
sha2 = { version = "0.10" }
|
||||||
ureq = { version = "3" }
|
ureq = { version = "3" }
|
||||||
clap = { version = "4.5" }
|
clap = { version = "4.5" }
|
||||||
|
jkcli = { version = "0.1" }
|
||||||
plist = { version = "1.7" }
|
plist = { version = "1.7" }
|
||||||
|
plist-macro = { version = "0.1.3" }
|
||||||
ns-keyed-archive = "0.1.2"
|
ns-keyed-archive = "0.1.2"
|
||||||
uuid = "1.16"
|
uuid = "1.16"
|
||||||
futures-util = { version = "0.3" }
|
futures-util = { version = "0.3" }
|
||||||
|
|||||||
@@ -1,65 +1,23 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient,
|
IdeviceService, lockdown::LockdownClient, mobileactivationd::MobileActivationdClient,
|
||||||
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Manage activation status on an iOS device")
|
||||||
async fn main() {
|
.with_subcommand("state", JkCommand::new().help("Gets the activation state"))
|
||||||
tracing_subscriber::fmt::init();
|
.with_subcommand(
|
||||||
|
"deactivate",
|
||||||
let matches = Command::new("activation")
|
JkCommand::new().help("Deactivates the device"),
|
||||||
.about("mobileactivationd")
|
|
||||||
.arg(
|
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.subcommand_required(true)
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let activation_client = MobileActivationdClient::new(&*provider);
|
let activation_client = MobileActivationdClient::new(&*provider);
|
||||||
let mut lc = LockdownClient::connect(&*provider)
|
let mut lc = LockdownClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
@@ -74,40 +32,19 @@ async fn main() {
|
|||||||
.into_string()
|
.into_string()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if matches.subcommand_matches("state").is_some() {
|
let (sub_name, _sub_args) = arguments.first_subcommand().expect("no subarg passed");
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"state" => {
|
||||||
let s = activation_client.state().await.expect("no state");
|
let s = activation_client.state().await.expect("no state");
|
||||||
println!("Activation State: {s}");
|
println!("Activation State: {s}");
|
||||||
} else if matches.subcommand_matches("deactivate").is_some() {
|
}
|
||||||
|
"deactivate" => {
|
||||||
println!("CAUTION: You are deactivating {udid}, press enter to continue.");
|
println!("CAUTION: You are deactivating {udid}, press enter to continue.");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin().read_line(&mut input).ok();
|
std::io::stdin().read_line(&mut input).ok();
|
||||||
activation_client.deactivate().await.expect("no deactivate");
|
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;
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
244
tools/src/afc.rs
244
tools/src/afc.rs
@@ -2,130 +2,119 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Arg, Command, value_parser};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService,
|
IdeviceService,
|
||||||
afc::{AfcClient, opcode::AfcFopenMode},
|
afc::{AfcClient, opcode::AfcFopenMode},
|
||||||
house_arrest::HouseArrestClient,
|
house_arrest::HouseArrestClient,
|
||||||
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
|
|
||||||
mod common;
|
const DOCS_HELP: &str = "Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents";
|
||||||
|
|
||||||
#[tokio::main]
|
pub fn register() -> JkCommand {
|
||||||
async fn main() {
|
JkCommand::new()
|
||||||
tracing_subscriber::fmt::init();
|
.help("Manage files in the AFC jail of a device")
|
||||||
|
.with_flag(
|
||||||
let matches = Command::new("afc")
|
JkFlag::new("documents")
|
||||||
.about("Manage files on the device")
|
.with_help(DOCS_HELP)
|
||||||
.arg(
|
.with_argument(JkArgument::new().required(true)),
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_flag(
|
||||||
Arg::new("pairing_file")
|
JkFlag::new("container")
|
||||||
.long("pairing-file")
|
.with_help("Read the container contents of a bundle")
|
||||||
.value_name("PATH")
|
.with_argument(JkArgument::new().required(true)),
|
||||||
.help("Path to the pairing file"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("udid")
|
"list",
|
||||||
.long("udid")
|
JkCommand::new()
|
||||||
.value_name("UDID")
|
.help("Lists the items in the directory")
|
||||||
.help("UDID of the device (overrides host/pairing file)"),
|
.with_argument(
|
||||||
)
|
JkArgument::new()
|
||||||
.arg(
|
|
||||||
Arg::new("documents")
|
|
||||||
.long("documents")
|
|
||||||
.value_name("BUNDLE_ID")
|
|
||||||
.help("Read the documents from a bundle. Note that when vending documents, you can only access files in /Documents")
|
|
||||||
.global(true),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("container")
|
|
||||||
.long("container")
|
|
||||||
.value_name("BUNDLE_ID")
|
|
||||||
.help("Read the container contents of a bundle")
|
|
||||||
.global(true),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("list")
|
|
||||||
.about("Lists the items in the directory")
|
|
||||||
.arg(Arg::new("path").required(true).index(1)),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("download")
|
|
||||||
.about("Downloads a file")
|
|
||||||
.arg(Arg::new("path").required(true).index(1))
|
|
||||||
.arg(Arg::new("save").required(true).index(2)),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("upload")
|
|
||||||
.about("Creates a directory")
|
|
||||||
.arg(
|
|
||||||
Arg::new("file")
|
|
||||||
.required(true)
|
.required(true)
|
||||||
.index(1)
|
.with_help("The directory to list in"),
|
||||||
.value_parser(value_parser!(PathBuf)),
|
),
|
||||||
)
|
)
|
||||||
.arg(Arg::new("path").required(true).index(2)),
|
.with_subcommand(
|
||||||
|
"download",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Download a file")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path in the AFC jail"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_argument(
|
||||||
Command::new("mkdir")
|
JkArgument::new()
|
||||||
.about("Creates a directory")
|
.required(true)
|
||||||
.arg(Arg::new("path").required(true).index(1)),
|
.with_help("Path to save file to"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("remove")
|
"upload",
|
||||||
.about("Remove a provisioning profile")
|
JkCommand::new()
|
||||||
.arg(Arg::new("path").required(true).index(1)),
|
.help("Upload a file")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path to the file to upload"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_argument(
|
||||||
Command::new("remove_all")
|
JkArgument::new()
|
||||||
.about("Remove a provisioning profile")
|
.required(true)
|
||||||
.arg(Arg::new("path").required(true).index(1)),
|
.with_help("Path to save file to in the AFC jail"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("info")
|
"mkdir",
|
||||||
.about("Get info about a file")
|
JkCommand::new().help("Create a folder").with_argument(
|
||||||
.arg(Arg::new("path").required(true).index(1)),
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path to the folder to create in the AFC jail"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("device_info").about("Get info about the device"))
|
.with_subcommand(
|
||||||
.get_matches();
|
"remove",
|
||||||
|
JkCommand::new().help("Remove a file").with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path to the file to remove"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"remove_all",
|
||||||
|
JkCommand::new().help("Remove a folder").with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path to the folder to remove"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"info",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Get info about a file")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path to the file to get info for"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"device_info",
|
||||||
|
JkCommand::new().help("Get info about the device"),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
println!("afc");
|
let mut afc_client = if let Some(bundle_id) = arguments.get_flag::<String>("container") {
|
||||||
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, "afc-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut afc_client = if let Some(bundle_id) = matches.get_one::<String>("container") {
|
|
||||||
let h = HouseArrestClient::connect(&*provider)
|
let h = HouseArrestClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to connect to house arrest");
|
.expect("Failed to connect to house arrest");
|
||||||
h.vend_container(bundle_id)
|
h.vend_container(bundle_id)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to vend container")
|
.expect("Failed to vend container")
|
||||||
} else if let Some(bundle_id) = matches.get_one::<String>("documents") {
|
} else if let Some(bundle_id) = arguments.get_flag::<String>("documents") {
|
||||||
let h = HouseArrestClient::connect(&*provider)
|
let h = HouseArrestClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to connect to house arrest");
|
.expect("Failed to connect to house arrest");
|
||||||
@@ -138,16 +127,24 @@ async fn main() {
|
|||||||
.expect("Unable to connect to misagent")
|
.expect("Unable to connect to misagent")
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("list") {
|
let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
let mut sub_args = sub_args.clone();
|
||||||
let res = afc_client.list_dir(path).await.expect("Failed to read dir");
|
match sub_name.as_str() {
|
||||||
|
"list" => {
|
||||||
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
|
let res = afc_client
|
||||||
|
.list_dir(&path)
|
||||||
|
.await
|
||||||
|
.expect("Failed to read dir");
|
||||||
println!("{path}\n{res:#?}");
|
println!("{path}\n{res:#?}");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("mkdir") {
|
}
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
"mkdir" => {
|
||||||
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
afc_client.mk_dir(path).await.expect("Failed to mkdir");
|
afc_client.mk_dir(path).await.expect("Failed to mkdir");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("download") {
|
}
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
"download" => {
|
||||||
let save = matches.get_one::<String>("save").expect("No path passed");
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
|
let save = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
|
|
||||||
let mut file = afc_client
|
let mut file = afc_client
|
||||||
.open(path, AfcFopenMode::RdOnly)
|
.open(path, AfcFopenMode::RdOnly)
|
||||||
@@ -158,9 +155,10 @@ async fn main() {
|
|||||||
tokio::fs::write(save, res)
|
tokio::fs::write(save, res)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to write to file");
|
.expect("Failed to write to file");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("upload") {
|
}
|
||||||
let file = matches.get_one::<PathBuf>("file").expect("No path passed");
|
"upload" => {
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
let file = sub_args.next_argument::<PathBuf>().expect("No path passed");
|
||||||
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
|
|
||||||
let bytes = tokio::fs::read(file).await.expect("Failed to read file");
|
let bytes = tokio::fs::read(file).await.expect("Failed to read file");
|
||||||
let mut file = afc_client
|
let mut file = afc_client
|
||||||
@@ -171,26 +169,30 @@ async fn main() {
|
|||||||
file.write_entire(&bytes)
|
file.write_entire(&bytes)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to upload bytes");
|
.expect("Failed to upload bytes");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("remove") {
|
}
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
"remove" => {
|
||||||
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
afc_client.remove(path).await.expect("Failed to remove");
|
afc_client.remove(path).await.expect("Failed to remove");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("remove_all") {
|
}
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
"remove_all" => {
|
||||||
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
afc_client.remove_all(path).await.expect("Failed to remove");
|
afc_client.remove_all(path).await.expect("Failed to remove");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("info") {
|
}
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
"info" => {
|
||||||
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
let res = afc_client
|
let res = afc_client
|
||||||
.get_file_info(path)
|
.get_file_info(path)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get file info");
|
.expect("Failed to get file info");
|
||||||
println!("{res:#?}");
|
println!("{res:#?}");
|
||||||
} else if matches.subcommand_matches("device_info").is_some() {
|
}
|
||||||
|
"device_info" => {
|
||||||
let res = afc_client
|
let res = afc_client
|
||||||
.get_device_info()
|
.get_device_info()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get file info");
|
.expect("Failed to get file info");
|
||||||
println!("{res:#?}");
|
println!("{res:#?}");
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,69 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{IdeviceService, amfi::AmfiClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, amfi::AmfiClient};
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Mess with devleoper mode")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"show",
|
||||||
|
JkCommand::new().help("Shows the developer mode option in settings"),
|
||||||
let matches = Command::new("amfi")
|
|
||||||
.about("Mess with developer mode")
|
|
||||||
.arg(
|
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand("enable", JkCommand::new().help("Enables developer mode"))
|
||||||
Arg::new("pairing_file")
|
.with_subcommand(
|
||||||
.long("pairing-file")
|
"accept",
|
||||||
.value_name("PATH")
|
JkCommand::new().help("Shows the accept dialogue for developer mode"),
|
||||||
.help("Path to the pairing file"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("udid")
|
"status",
|
||||||
.value_name("UDID")
|
JkCommand::new().help("Gets the developer mode status"),
|
||||||
.help("UDID of the device (overrides host/pairing file)")
|
|
||||||
.index(1),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("about")
|
"trust",
|
||||||
.long("about")
|
JkCommand::new()
|
||||||
.help("Show about information")
|
.help("Trusts an app signer")
|
||||||
.action(clap::ArgAction::SetTrue),
|
.with_argument(JkArgument::new().with_help("UUID").required(true)),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("show").about("Shows the developer mode option in settings"))
|
.subcommand_required(true)
|
||||||
.subcommand(Command::new("enable").about("Enables developer mode"))
|
}
|
||||||
.subcommand(Command::new("accept").about("Shows the accept dialogue for developer mode"))
|
|
||||||
.subcommand(Command::new("status").about("Gets the developer mode status"))
|
|
||||||
.subcommand(
|
|
||||||
Command::new("trust")
|
|
||||||
.about("Trusts an app signer")
|
|
||||||
.arg(Arg::new("uuid").required(true)),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("amfi - manage developer mode");
|
|
||||||
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, "amfi-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut amfi_client = AmfiClient::connect(&*provider)
|
let mut amfi_client = AmfiClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to connect to amfi");
|
.expect("Failed to connect to amfi");
|
||||||
|
|
||||||
if matches.subcommand_matches("show").is_some() {
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"show" => {
|
||||||
amfi_client
|
amfi_client
|
||||||
.reveal_developer_mode_option_in_ui()
|
.reveal_developer_mode_option_in_ui()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to show");
|
.expect("Failed to show");
|
||||||
} else if matches.subcommand_matches("enable").is_some() {
|
}
|
||||||
|
"enable" => {
|
||||||
amfi_client
|
amfi_client
|
||||||
.enable_developer_mode()
|
.enable_developer_mode()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to show");
|
.expect("Failed to show");
|
||||||
} else if matches.subcommand_matches("accept").is_some() {
|
}
|
||||||
|
"accept" => {
|
||||||
amfi_client
|
amfi_client
|
||||||
.accept_developer_mode()
|
.accept_developer_mode()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to show");
|
.expect("Failed to show");
|
||||||
} else if matches.subcommand_matches("status").is_some() {
|
}
|
||||||
|
"status" => {
|
||||||
let status = amfi_client
|
let status = amfi_client
|
||||||
.get_developer_mode_status()
|
.get_developer_mode_status()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get status");
|
.expect("Failed to get status");
|
||||||
println!("Enabled: {status}");
|
println!("Enabled: {status}");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("state") {
|
}
|
||||||
let uuid: &String = match matches.get_one("uuid") {
|
"trust" => {
|
||||||
|
let uuid: String = match sub_args.next_argument() {
|
||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No UUID passed. Invalid usage, pass -h for help");
|
eprintln!("No UUID passed. Invalid usage, pass -h for help");
|
||||||
@@ -102,8 +75,7 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to get state");
|
.expect("Failed to get state");
|
||||||
println!("Enabled: {status}");
|
println!("Enabled: {status}");
|
||||||
} else {
|
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
|
||||||
}
|
}
|
||||||
return;
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +1,72 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, RsdService,
|
IdeviceService, RsdService,
|
||||||
core_device::{AppServiceClient, OpenStdioSocketClient},
|
core_device::{AppServiceClient, OpenStdioSocketClient},
|
||||||
core_device_proxy::CoreDeviceProxy,
|
core_device_proxy::CoreDeviceProxy,
|
||||||
|
provider::IdeviceProvider,
|
||||||
rsd::RsdHandshake,
|
rsd::RsdHandshake,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Interact with the RemoteXPC app service on the device")
|
||||||
async fn main() {
|
.with_subcommand("list", JkCommand::new().help("List apps on the device"))
|
||||||
tracing_subscriber::fmt::init();
|
.with_subcommand(
|
||||||
|
"launch",
|
||||||
let matches = Command::new("remotexpc")
|
JkCommand::new()
|
||||||
.about("Get services from RemoteXPC")
|
.help("Launch an app on the device")
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("host")
|
JkArgument::new()
|
||||||
.long("host")
|
.with_help("Bundle ID to launch")
|
||||||
.value_name("HOST")
|
.required(true),
|
||||||
.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("tunneld")
|
|
||||||
.long("tunneld")
|
|
||||||
.help("Use tunneld")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(Command::new("list").about("Lists the images mounted on the device"))
|
|
||||||
.subcommand(
|
|
||||||
Command::new("launch")
|
|
||||||
.about("Launch the app on the device")
|
|
||||||
.arg(
|
|
||||||
Arg::new("bundle_id")
|
|
||||||
.required(true)
|
|
||||||
.help("The bundle ID to launch"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("processes").about("List the processes running"))
|
.with_subcommand(
|
||||||
.subcommand(
|
"processes",
|
||||||
Command::new("uninstall").about("Uninstall an app").arg(
|
JkCommand::new().help("List the processes running"),
|
||||||
Arg::new("bundle_id")
|
)
|
||||||
.required(true)
|
.with_subcommand(
|
||||||
.help("The bundle ID to uninstall"),
|
"uninstall",
|
||||||
|
JkCommand::new().help("Uninstall an app").with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Bundle ID to uninstall")
|
||||||
|
.required(true),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("signal")
|
"signal",
|
||||||
.about("Send a signal to an app")
|
JkCommand::new()
|
||||||
.arg(Arg::new("pid").required(true).help("PID to send to"))
|
.help("Uninstall an app")
|
||||||
.arg(Arg::new("signal").required(true).help("Signal to send")),
|
.with_argument(JkArgument::new().with_help("PID to signal").required(true))
|
||||||
|
.with_argument(JkArgument::new().with_help("Signal to send").required(true)),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("icon")
|
"icon",
|
||||||
.about("Send a signal to an app")
|
JkCommand::new()
|
||||||
.arg(
|
.help("Fetch an icon for an app")
|
||||||
Arg::new("bundle_id")
|
.with_argument(
|
||||||
.required(true)
|
JkArgument::new()
|
||||||
.help("The bundle ID to fetch"),
|
.with_help("Bundle ID for the app")
|
||||||
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("path")
|
JkArgument::new()
|
||||||
.required(true)
|
.with_help("Path to save it to")
|
||||||
.help("The path to save the icon to"),
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(Arg::new("hw").required(false).help("The height and width"))
|
.with_argument(
|
||||||
.arg(Arg::new("scale").required(false).help("The scale")),
|
JkArgument::new()
|
||||||
|
.with_help("Height and width")
|
||||||
|
.required(true),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.with_argument(JkArgument::new().with_help("Scale").required(true)),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
println!("debug_proxy - connect to the debug proxy and run commands");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
|
||||||
let host = matches.get_one::<String>("host");
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
@@ -123,14 +84,19 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("no connect");
|
.expect("no connect");
|
||||||
|
|
||||||
if matches.subcommand_matches("list").is_some() {
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"list" => {
|
||||||
let apps = asc
|
let apps = asc
|
||||||
.list_apps(true, true, true, true, true)
|
.list_apps(true, true, true, true, true)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get apps");
|
.expect("Failed to get apps");
|
||||||
println!("{apps:#?}");
|
println!("{apps:#?}");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("launch") {
|
}
|
||||||
let bundle_id: &String = match matches.get_one("bundle_id") {
|
"launch" => {
|
||||||
|
let bundle_id: String = match sub_args.next_argument() {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle ID passed");
|
eprintln!("No bundle ID passed");
|
||||||
@@ -163,7 +129,6 @@ async fn main() {
|
|||||||
eprintln!("Error copying from remote to local: {}", e);
|
eprintln!("Error copying from remote to local: {}", e);
|
||||||
}
|
}
|
||||||
println!("\nRemote connection closed.");
|
println!("\nRemote connection closed.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Task 2: Copy data from local stdin to the remote process
|
// Task 2: Copy data from local stdin to the remote process
|
||||||
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => {
|
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => {
|
||||||
@@ -171,14 +136,15 @@ async fn main() {
|
|||||||
eprintln!("Error copying from local to remote: {}", e);
|
eprintln!("Error copying from local to remote: {}", e);
|
||||||
}
|
}
|
||||||
println!("\nLocal stdin closed.");
|
println!("\nLocal stdin closed.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if matches.subcommand_matches("processes").is_some() {
|
}
|
||||||
|
"processes" => {
|
||||||
let p = asc.list_processes().await.expect("no processes?");
|
let p = asc.list_processes().await.expect("no processes?");
|
||||||
println!("{p:#?}");
|
println!("{p:#?}");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("uninstall") {
|
}
|
||||||
let bundle_id: &String = match matches.get_one("bundle_id") {
|
"uninstall" => {
|
||||||
|
let bundle_id: String = match sub_args.next_argument() {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle ID passed");
|
eprintln!("No bundle ID passed");
|
||||||
@@ -187,16 +153,17 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
asc.uninstall_app(bundle_id).await.expect("no launch")
|
asc.uninstall_app(bundle_id).await.expect("no launch")
|
||||||
} else if let Some(matches) = matches.subcommand_matches("signal") {
|
}
|
||||||
let pid: u32 = match matches.get_one::<String>("pid") {
|
"signal" => {
|
||||||
Some(b) => b.parse().expect("failed to parse PID as u32"),
|
let pid: u32 = match sub_args.next_argument() {
|
||||||
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle PID passed");
|
eprintln!("No bundle PID passed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let signal: u32 = match matches.get_one::<String>("signal") {
|
let signal: u32 = match sub_args.next_argument() {
|
||||||
Some(b) => b.parse().expect("failed to parse signal as u32"),
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle signal passed");
|
eprintln!("No bundle signal passed");
|
||||||
return;
|
return;
|
||||||
@@ -205,29 +172,24 @@ async fn main() {
|
|||||||
|
|
||||||
let res = asc.send_signal(pid, signal).await.expect("no signal");
|
let res = asc.send_signal(pid, signal).await.expect("no signal");
|
||||||
println!("{res:#?}");
|
println!("{res:#?}");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("icon") {
|
}
|
||||||
let bundle_id: &String = match matches.get_one("bundle_id") {
|
"icon" => {
|
||||||
|
let bundle_id: String = match sub_args.next_argument() {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle ID passed");
|
eprintln!("No bundle ID passed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let save_path: &String = match matches.get_one("path") {
|
let save_path: String = match sub_args.next_argument() {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle ID passed");
|
eprintln!("No bundle ID passed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let hw: f32 = match matches.get_one::<String>("hw") {
|
let hw: f32 = sub_args.next_argument().unwrap_or(1.0);
|
||||||
Some(b) => b.parse().expect("failed to parse PID as f32"),
|
let scale: f32 = sub_args.next_argument().unwrap_or(1.0);
|
||||||
None => 1.0,
|
|
||||||
};
|
|
||||||
let scale: f32 = match matches.get_one::<String>("scale") {
|
|
||||||
Some(b) => b.parse().expect("failed to parse signal as f32"),
|
|
||||||
None => 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = asc
|
let res = asc
|
||||||
.fetch_app_icon(bundle_id, hw, hw, scale, true)
|
.fetch_app_icon(bundle_id, hw, hw, scale, true)
|
||||||
@@ -237,7 +199,7 @@ async fn main() {
|
|||||||
tokio::fs::write(save_path, res.data)
|
tokio::fs::write(save_path, res.data)
|
||||||
.await
|
.await
|
||||||
.expect("failed to save");
|
.expect("failed to save");
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,20 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient};
|
use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient, provider::IdeviceProvider};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
use tokio::io::AsyncWrite;
|
use tokio::io::AsyncWrite;
|
||||||
|
|
||||||
use crate::pcap::{write_pcap_header, write_pcap_record};
|
use crate::pcap::{write_pcap_header, write_pcap_record};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
mod pcap;
|
JkCommand::new()
|
||||||
|
.help("Writes Bluetooth pcap data")
|
||||||
|
.with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)"))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
let out: Option<String> = arguments.clone().next_argument();
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("amfi")
|
|
||||||
.about("Capture Bluetooth packets")
|
|
||||||
.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),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("out")
|
|
||||||
.long("out")
|
|
||||||
.value_name("PCAP")
|
|
||||||
.help("Write PCAP to this file (use '-' for stdout)"),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("bt_packet_logger - capture bluetooth packets");
|
|
||||||
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 out = matches.get_one::<String>("out").map(String::to_owned);
|
|
||||||
|
|
||||||
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let logger_client = BtPacketLoggerClient::connect(&*provider)
|
let logger_client = BtPacketLoggerClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,83 +1,60 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command, arg};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, RsdService, companion_proxy::CompanionProxy,
|
IdeviceService, RsdService, companion_proxy::CompanionProxy,
|
||||||
core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist,
|
core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake,
|
||||||
rsd::RsdHandshake,
|
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
use plist_macro::{pretty_print_dictionary, pretty_print_plist};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Apple Watch proxy")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"list",
|
||||||
|
JkCommand::new().help("List the companions on the device"),
|
||||||
let matches = Command::new("companion_proxy")
|
)
|
||||||
.about("Apple Watch things")
|
.with_subcommand("listen", JkCommand::new().help("Listen for devices"))
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("host")
|
"get",
|
||||||
.long("host")
|
JkCommand::new()
|
||||||
.value_name("HOST")
|
.help("Gets a value from an AW")
|
||||||
.help("IP address of the device"),
|
.with_argument(
|
||||||
)
|
JkArgument::new()
|
||||||
.arg(
|
.with_help("The AW UDID to get from")
|
||||||
Arg::new("pairing_file")
|
.required(true),
|
||||||
.long("pairing-file")
|
)
|
||||||
.value_name("PATH")
|
.with_argument(
|
||||||
.help("Path to the pairing file"),
|
JkArgument::new()
|
||||||
)
|
.with_help("The value to get")
|
||||||
.arg(
|
.required(true),
|
||||||
Arg::new("udid")
|
),
|
||||||
.value_name("UDID")
|
)
|
||||||
.help("UDID of the device (overrides host/pairing file)")
|
.with_subcommand(
|
||||||
.index(1),
|
"start",
|
||||||
)
|
JkCommand::new()
|
||||||
.arg(
|
.help("Starts a service on the Apple Watch")
|
||||||
Arg::new("about")
|
.with_argument(
|
||||||
.long("about")
|
JkArgument::new()
|
||||||
.help("Show about information")
|
.with_help("The port to listen on")
|
||||||
.action(clap::ArgAction::SetTrue),
|
.required(true),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("list").about("List the companions on the device"))
|
.with_argument(JkArgument::new().with_help("The service name")),
|
||||||
.subcommand(Command::new("listen").about("Listen for devices"))
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("get")
|
"stop",
|
||||||
.about("Gets a value")
|
JkCommand::new()
|
||||||
.arg(arg!(-d --device_udid <STRING> "the device udid to get from").required(true))
|
.help("Stops a service on the Apple Watch")
|
||||||
.arg(arg!(-v --value <STRING> "the value to get").required(true)),
|
.with_argument(
|
||||||
)
|
JkArgument::new()
|
||||||
.subcommand(
|
.with_help("The port to stop")
|
||||||
Command::new("start")
|
.required(true),
|
||||||
.about("Starts a service")
|
),
|
||||||
.arg(arg!(-p --port <PORT> "the port").required(true))
|
)
|
||||||
.arg(arg!(-n --name <STRING> "the optional service name").required(false)),
|
.subcommand_required(true)
|
||||||
)
|
}
|
||||||
.subcommand(
|
|
||||||
Command::new("stop")
|
|
||||||
.about("Starts a service")
|
|
||||||
.arg(arg!(-p --port <PORT> "the port").required(true)),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("companion_proxy");
|
|
||||||
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, "amfi-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core_device_proxy");
|
.expect("no core_device_proxy");
|
||||||
@@ -97,17 +74,23 @@ async fn main() {
|
|||||||
// .await
|
// .await
|
||||||
// .expect("Failed to connect to companion proxy");
|
// .expect("Failed to connect to companion proxy");
|
||||||
|
|
||||||
if matches.subcommand_matches("list").is_some() {
|
let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"list" => {
|
||||||
proxy.get_device_registry().await.expect("Failed to show");
|
proxy.get_device_registry().await.expect("Failed to show");
|
||||||
} else if matches.subcommand_matches("listen").is_some() {
|
}
|
||||||
|
"listen" => {
|
||||||
let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
|
let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
|
||||||
while let Ok(v) = stream.next().await {
|
while let Ok(v) = stream.next().await {
|
||||||
println!("{}", pretty_print_dictionary(&v));
|
println!("{}", pretty_print_dictionary(&v));
|
||||||
}
|
}
|
||||||
} else if let Some(matches) = matches.subcommand_matches("get") {
|
}
|
||||||
let key = matches.get_one::<String>("value").expect("no value passed");
|
"get" => {
|
||||||
let udid = matches
|
let key: String = sub_args.next_argument::<String>().expect("no value passed");
|
||||||
.get_one::<String>("device_udid")
|
let udid = sub_args
|
||||||
|
.next_argument::<String>()
|
||||||
.expect("no AW udid passed");
|
.expect("no AW udid passed");
|
||||||
|
|
||||||
match proxy.get_value(udid, key).await {
|
match proxy.get_value(udid, key).await {
|
||||||
@@ -118,15 +101,26 @@ async fn main() {
|
|||||||
eprintln!("Error getting value: {e}");
|
eprintln!("Error getting value: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(matches) = matches.subcommand_matches("start") {
|
}
|
||||||
let port: u16 = matches
|
"start" => {
|
||||||
.get_one::<String>("port")
|
let port: u16 = sub_args
|
||||||
|
.next_argument::<String>()
|
||||||
.expect("no port passed")
|
.expect("no port passed")
|
||||||
.parse()
|
.parse()
|
||||||
.expect("not a number");
|
.expect("not a number");
|
||||||
let name = matches.get_one::<String>("name").map(|x| x.as_str());
|
let name = sub_args.next_argument::<String>();
|
||||||
|
|
||||||
match proxy.start_forwarding_service_port(port, name, None).await {
|
match proxy
|
||||||
|
.start_forwarding_service_port(
|
||||||
|
port,
|
||||||
|
match &name {
|
||||||
|
Some(n) => Some(n.as_str()),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
println!("started on port {value}");
|
println!("started on port {value}");
|
||||||
}
|
}
|
||||||
@@ -134,9 +128,10 @@ async fn main() {
|
|||||||
eprintln!("Error starting: {e}");
|
eprintln!("Error starting: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(matches) = matches.subcommand_matches("stop") {
|
}
|
||||||
let port: u16 = matches
|
"stop" => {
|
||||||
.get_one::<String>("port")
|
let port: u16 = sub_args
|
||||||
|
.next_argument::<String>()
|
||||||
.expect("no port passed")
|
.expect("no port passed")
|
||||||
.parse()
|
.parse()
|
||||||
.expect("not a number");
|
.expect("not a number");
|
||||||
@@ -144,8 +139,7 @@ async fn main() {
|
|||||||
if let Err(e) = proxy.stop_forwarding_service_port(port).await {
|
if let Err(e) = proxy.stop_forwarding_service_port(port).await {
|
||||||
eprintln!("Error starting: {e}");
|
eprintln!("Error starting: {e}");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
|
||||||
}
|
}
|
||||||
return;
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,79 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService,
|
IdeviceService,
|
||||||
crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
|
crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
|
||||||
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Manage crash logs")
|
||||||
|
.with_subcommand(
|
||||||
|
"list",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("List crash logs in the directory")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Path to list in")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"flush",
|
||||||
|
JkCommand::new().help("Flushes reports to the directory"),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"pull",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Check the capabilities")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Path to the log to pull")
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Path to save the log to")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("crash_logs")
|
|
||||||
.about("Manage crash logs")
|
|
||||||
.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)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("list")
|
|
||||||
.about("Lists the items in the directory")
|
|
||||||
.arg(Arg::new("dir").required(false).index(1)),
|
|
||||||
)
|
|
||||||
.subcommand(Command::new("flush").about("Flushes reports to the directory"))
|
|
||||||
.subcommand(
|
|
||||||
Command::new("pull")
|
|
||||||
.about("Pulls a log")
|
|
||||||
.arg(Arg::new("path").required(true).index(1))
|
|
||||||
.arg(Arg::new("save").required(true).index(2))
|
|
||||||
.arg(Arg::new("dir").required(false).index(3)),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("crash_logs - manage crash logs on 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, "afc-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut crash_client = CrashReportCopyMobileClient::connect(&*provider)
|
let mut crash_client = CrashReportCopyMobileClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to misagent");
|
.expect("Unable to connect to misagent");
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("list") {
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub command passed");
|
||||||
let dir_path: Option<&String> = matches.get_one("dir");
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"list" => {
|
||||||
|
let dir_path: Option<String> = sub_args.next_argument();
|
||||||
let res = crash_client
|
let res = crash_client
|
||||||
.ls(dir_path.map(|x| x.as_str()))
|
.ls(match &dir_path {
|
||||||
|
Some(d) => Some(d.as_str()),
|
||||||
|
None => None,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.expect("Failed to read dir");
|
.expect("Failed to read dir");
|
||||||
println!("{res:#?}");
|
println!("{res:#?}");
|
||||||
} else if matches.subcommand_matches("flush").is_some() {
|
}
|
||||||
|
"flush" => {
|
||||||
flush_reports(&*provider).await.expect("Failed to flush");
|
flush_reports(&*provider).await.expect("Failed to flush");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("pull") {
|
}
|
||||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
"pull" => {
|
||||||
let save = matches.get_one::<String>("save").expect("No path passed");
|
let path = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
|
let save = sub_args.next_argument::<String>().expect("No path passed");
|
||||||
|
|
||||||
let res = crash_client.pull(path).await.expect("Failed to pull log");
|
let res = crash_client.pull(path).await.expect("Failed to pull log");
|
||||||
tokio::fs::write(save, res)
|
tokio::fs::write(save, res)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to write to file");
|
.expect("Failed to write to file");
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,70 +2,17 @@
|
|||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient,
|
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient,
|
||||||
rsd::RsdHandshake,
|
provider::IdeviceProvider, rsd::RsdHandshake,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("Start a debug proxy shell")
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("remotexpc")
|
|
||||||
.about("Get services from RemoteXPC")
|
|
||||||
.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("tunneld")
|
|
||||||
.long("tunneld")
|
|
||||||
.help("Use tunneld")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("debug_proxy - connect to the debug proxy and run commands");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
|
||||||
let host = matches.get_one::<String>("host");
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
|
|||||||
@@ -1,106 +1,71 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// idevice Rust implementation of libimobiledevice's idevicediagnostics
|
// idevice Rust implementation of libimobiledevice's idevicediagnostics
|
||||||
|
|
||||||
use clap::{Arg, ArgMatches, Command};
|
use idevice::{
|
||||||
use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient};
|
IdeviceService, provider::IdeviceProvider, services::diagnostics_relay::DiagnosticsRelayClient,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Interact with the diagnostics interface of a device")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"ioregistry",
|
||||||
|
JkCommand::new()
|
||||||
let matches = Command::new("idevicediagnostics")
|
.help("Print IORegistry information")
|
||||||
.about("Interact with the diagnostics interface of a device")
|
.with_flag(
|
||||||
.arg(
|
JkFlag::new("plane")
|
||||||
Arg::new("host")
|
.with_help("IORegistry plane to query (e.g., IODeviceTree, IOService)")
|
||||||
.long("host")
|
.with_argument(JkArgument::new().required(true)),
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_flag(
|
||||||
Arg::new("pairing_file")
|
JkFlag::new("name")
|
||||||
.long("pairing-file")
|
.with_help("Entry name to filter by")
|
||||||
.value_name("PATH")
|
.with_argument(JkArgument::new().required(true)),
|
||||||
.help("Path to the pairing file"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_flag(
|
||||||
Arg::new("udid")
|
JkFlag::new("class")
|
||||||
.value_name("UDID")
|
.with_help("Entry class to filter by")
|
||||||
.help("UDID of the device (overrides host/pairing file)")
|
.with_argument(JkArgument::new().required(true)),
|
||||||
.index(1),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("ioregistry")
|
|
||||||
.about("Print IORegistry information")
|
|
||||||
.arg(
|
|
||||||
Arg::new("plane")
|
|
||||||
.long("plane")
|
|
||||||
.value_name("PLANE")
|
|
||||||
.help("IORegistry plane to query (e.g., IODeviceTree, IOService)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("name")
|
|
||||||
.long("name")
|
|
||||||
.value_name("NAME")
|
|
||||||
.help("Entry name to filter by"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("class")
|
|
||||||
.long("class")
|
|
||||||
.value_name("CLASS")
|
|
||||||
.help("Entry class to filter by"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("mobilegestalt")
|
"mobilegestalt",
|
||||||
.about("Print MobileGestalt information")
|
JkCommand::new()
|
||||||
.arg(
|
.help("Print MobileGestalt information")
|
||||||
Arg::new("keys")
|
.with_argument(
|
||||||
.long("keys")
|
JkArgument::new()
|
||||||
.value_name("KEYS")
|
.with_help("Comma-separated list of keys to query")
|
||||||
.help("Comma-separated list of keys to query")
|
.required(true),
|
||||||
.value_delimiter(',')
|
|
||||||
.num_args(1..),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("gasguage").about("Print gas gauge (battery) information"))
|
.with_subcommand(
|
||||||
.subcommand(Command::new("nand").about("Print NAND flash information"))
|
"gasguage",
|
||||||
.subcommand(Command::new("all").about("Print all available diagnostics information"))
|
JkCommand::new().help("Print gas gauge (battery) information"),
|
||||||
.subcommand(Command::new("wifi").about("Print WiFi diagnostics information"))
|
)
|
||||||
.subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay"))
|
.with_subcommand(
|
||||||
.subcommand(Command::new("restart").about("Restart the device"))
|
"nand",
|
||||||
.subcommand(Command::new("shutdown").about("Shutdown the device"))
|
JkCommand::new().help("Print NAND flash information"),
|
||||||
.subcommand(Command::new("sleep").about("Put the device to sleep"))
|
)
|
||||||
.get_matches();
|
.with_subcommand(
|
||||||
|
"all",
|
||||||
if matches.get_flag("about") {
|
JkCommand::new().help("Print all available diagnostics information"),
|
||||||
println!(
|
)
|
||||||
"idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary."
|
.with_subcommand(
|
||||||
);
|
"wifi",
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
JkCommand::new().help("Print WiFi diagnostics information"),
|
||||||
return;
|
)
|
||||||
}
|
.with_subcommand(
|
||||||
|
"goodbye",
|
||||||
let udid = matches.get_one::<String>("udid");
|
JkCommand::new().help("Send Goodbye to diagnostics relay"),
|
||||||
let host = matches.get_one::<String>("host");
|
)
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
.with_subcommand("restart", JkCommand::new().help("Restart the device"))
|
||||||
|
.with_subcommand("shutdown", JkCommand::new().help("Shutdown the device"))
|
||||||
let provider =
|
.with_subcommand("sleep", JkCommand::new().help("Put the device to sleep"))
|
||||||
match common::get_provider(udid, host, pairing_file, "idevicediagnostics-jkcoxson").await {
|
.subcommand_required(true)
|
||||||
Ok(p) => p,
|
}
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
|
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -109,47 +74,52 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match matches.subcommand() {
|
let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
Some(("ioregistry", sub_matches)) => {
|
let mut sub_matches = sub_args.clone();
|
||||||
handle_ioregistry(&mut diagnostics_client, sub_matches).await;
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"ioregistry" => {
|
||||||
|
handle_ioregistry(&mut diagnostics_client, &sub_matches).await;
|
||||||
}
|
}
|
||||||
Some(("mobilegestalt", sub_matches)) => {
|
"mobilegestalt" => {
|
||||||
handle_mobilegestalt(&mut diagnostics_client, sub_matches).await;
|
handle_mobilegestalt(&mut diagnostics_client, &mut sub_matches).await;
|
||||||
}
|
}
|
||||||
Some(("gasguage", _)) => {
|
"gasguage" => {
|
||||||
handle_gasguage(&mut diagnostics_client).await;
|
handle_gasguage(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("nand", _)) => {
|
"nand" => {
|
||||||
handle_nand(&mut diagnostics_client).await;
|
handle_nand(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("all", _)) => {
|
"all" => {
|
||||||
handle_all(&mut diagnostics_client).await;
|
handle_all(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("wifi", _)) => {
|
"wifi" => {
|
||||||
handle_wifi(&mut diagnostics_client).await;
|
handle_wifi(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("restart", _)) => {
|
"restart" => {
|
||||||
handle_restart(&mut diagnostics_client).await;
|
handle_restart(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("shutdown", _)) => {
|
"shutdown" => {
|
||||||
handle_shutdown(&mut diagnostics_client).await;
|
handle_shutdown(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("sleep", _)) => {
|
"sleep" => {
|
||||||
handle_sleep(&mut diagnostics_client).await;
|
handle_sleep(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
Some(("goodbye", _)) => {
|
"goodbye" => {
|
||||||
handle_goodbye(&mut diagnostics_client).await;
|
handle_goodbye(&mut diagnostics_client).await;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => unreachable!(),
|
||||||
eprintln!("No subcommand specified. Use --help for usage information.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
|
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &CollectedArguments) {
|
||||||
let plane = matches.get_one::<String>("plane").map(|s| s.as_str());
|
let plane = matches.get_flag::<String>("plane");
|
||||||
let name = matches.get_one::<String>("name").map(|s| s.as_str());
|
let name = matches.get_flag::<String>("name");
|
||||||
let class = matches.get_one::<String>("class").map(|s| s.as_str());
|
let class = matches.get_flag::<String>("class");
|
||||||
|
|
||||||
|
let plane = plane.as_deref();
|
||||||
|
let name = name.as_deref();
|
||||||
|
let class = class.as_deref();
|
||||||
|
|
||||||
match client.ioregistry(plane, name, class).await {
|
match client.ioregistry(plane, name, class).await {
|
||||||
Ok(Some(data)) => {
|
Ok(Some(data)) => {
|
||||||
@@ -164,12 +134,14 @@ async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
|
async fn handle_mobilegestalt(
|
||||||
let keys = matches
|
client: &mut DiagnosticsRelayClient,
|
||||||
.get_many::<String>("keys")
|
matches: &mut CollectedArguments,
|
||||||
.map(|values| values.map(|s| s.to_string()).collect::<Vec<_>>());
|
) {
|
||||||
|
let keys = matches.next_argument::<String>().unwrap();
|
||||||
|
let keys = keys.split(',').map(|x| x.to_string()).collect();
|
||||||
|
|
||||||
match client.mobilegestalt(keys).await {
|
match client.mobilegestalt(Some(keys)).await {
|
||||||
Ok(Some(data)) => {
|
Ok(Some(data)) => {
|
||||||
println!("{data:#?}");
|
println!("{data:#?}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,18 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, RsdService, core_device::DiagnostisServiceClient,
|
IdeviceService, RsdService, core_device::DiagnostisServiceClient,
|
||||||
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake,
|
core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider, rsd::RsdHandshake,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("Retrieve a sysdiagnose")
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("remotexpc")
|
|
||||||
.about("Gets a sysdiagnose")
|
|
||||||
.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("tunneld")
|
|
||||||
.long("tunneld")
|
|
||||||
.help("Use tunneld")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("debug_proxy - connect to the debug proxy and run commands");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
|
||||||
let host = matches.get_one::<String>("host");
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use idevice::dvt::message::Message;
|
use idevice::{dvt::message::Message, provider::IdeviceProvider};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
#[tokio::main]
|
pub fn register() -> JkCommand {
|
||||||
async fn main() {
|
JkCommand::new()
|
||||||
let file = std::env::args().nth(1).expect("No file passed");
|
.help("Parse a DVT packet from a file")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path the the packet file"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, _provider: Box<dyn IdeviceProvider>) {
|
||||||
|
let mut arguments = arguments.clone();
|
||||||
|
|
||||||
|
let file: String = arguments.next_argument().expect("No file passed");
|
||||||
let mut bytes = tokio::fs::File::open(file).await.unwrap();
|
let mut bytes = tokio::fs::File::open(file).await.unwrap();
|
||||||
|
|
||||||
let message = Message::from_reader(&mut bytes).await.unwrap();
|
let message = Message::from_reader(&mut bytes).await.unwrap();
|
||||||
|
|||||||
@@ -1,60 +1,14 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// Heartbeat client
|
// Heartbeat client
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{IdeviceService, heartbeat::HeartbeatClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, heartbeat::HeartbeatClient};
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("heartbeat a device")
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
let matches = Command::new("core_device_proxy_tun")
|
|
||||||
.about("Start a tunnel")
|
|
||||||
.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") {
|
|
||||||
println!("heartbeat_client - heartbeat a 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, "heartbeat_client-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut heartbeat_client = HeartbeatClient::connect(&*provider)
|
let mut heartbeat_client = HeartbeatClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to heartbeat");
|
.expect("Unable to connect to heartbeat");
|
||||||
|
|||||||
@@ -1,64 +1,14 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// idevice Rust implementation of libimobiledevice's ideviceinfo
|
// idevice Rust implementation of libimobiledevice's ideviceinfo
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, lockdown::LockdownClient};
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.")
|
||||||
#[tokio::main]
|
}
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("core_device_proxy_tun")
|
|
||||||
.about("Start a tunnel")
|
|
||||||
.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") {
|
|
||||||
println!(
|
|
||||||
"ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary."
|
|
||||||
);
|
|
||||||
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, "ideviceinfo-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut lockdown_client = match LockdownClient::connect(&*provider).await {
|
let mut lockdown_client = match LockdownClient::connect(&*provider).await {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -1,72 +1,41 @@
|
|||||||
// A minimal ideviceinstaller-like CLI to install/upgrade apps
|
// A minimal ideviceinstaller-like CLI to install/upgrade apps
|
||||||
|
|
||||||
use clap::{Arg, ArgAction, Command};
|
use idevice::{provider::IdeviceProvider, utils::installation};
|
||||||
use idevice::utils::installation;
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Manage files in the AFC jail of a device")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"install",
|
||||||
|
JkCommand::new()
|
||||||
let matches = Command::new("ideviceinstaller")
|
.help("Install a local .ipa or directory")
|
||||||
.about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)")
|
.with_argument(
|
||||||
.arg(
|
JkArgument::new()
|
||||||
Arg::new("host")
|
.required(true)
|
||||||
.long("host")
|
.with_help("Path to the .ipa or directory containing the app"),
|
||||||
.value_name("HOST")
|
),
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("pairing_file")
|
"upgrade",
|
||||||
.long("pairing-file")
|
JkCommand::new()
|
||||||
.value_name("PATH")
|
.help("Install a local .ipa or directory")
|
||||||
.help("Path to the pairing file"),
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path to the .ipa or directory containing the app"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.subcommand_required(true)
|
||||||
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(ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("install")
|
|
||||||
.about("Install a local .ipa or directory")
|
|
||||||
.arg(Arg::new("path").required(true).value_name("PATH")),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("upgrade")
|
|
||||||
.about("Upgrade from a local .ipa or directory")
|
|
||||||
.arg(Arg::new("path").required(true).value_name("PATH")),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)");
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg");
|
||||||
println!("Copyright (c) 2025");
|
let mut sub_args = sub_args.clone();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
match sub_name.as_str() {
|
||||||
let host = matches.get_one::<String>("host");
|
"install" => {
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
let path: String = sub_args.next_argument().expect("required");
|
||||||
|
|
||||||
let provider = match common::get_provider(udid, host, pairing_file, "ideviceinstaller").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("install") {
|
|
||||||
let path: &String = matches.get_one("path").expect("required");
|
|
||||||
match installation::install_package_with_callback(
|
match installation::install_package_with_callback(
|
||||||
&*provider,
|
&*provider,
|
||||||
path,
|
path,
|
||||||
@@ -81,8 +50,9 @@ async fn main() {
|
|||||||
Ok(()) => println!("install success"),
|
Ok(()) => println!("install success"),
|
||||||
Err(e) => eprintln!("Install failed: {e}"),
|
Err(e) => eprintln!("Install failed: {e}"),
|
||||||
}
|
}
|
||||||
} else if let Some(matches) = matches.subcommand_matches("upgrade") {
|
}
|
||||||
let path: &String = matches.get_one("path").expect("required");
|
"upgrade" => {
|
||||||
|
let path: String = sub_args.next_argument().expect("required");
|
||||||
match installation::upgrade_package_with_callback(
|
match installation::upgrade_package_with_callback(
|
||||||
&*provider,
|
&*provider,
|
||||||
path,
|
path,
|
||||||
@@ -97,7 +67,7 @@ async fn main() {
|
|||||||
Ok(()) => println!("upgrade success"),
|
Ok(()) => println!("upgrade success"),
|
||||||
Err(e) => eprintln!("Upgrade failed: {e}"),
|
Err(e) => eprintln!("Upgrade failed: {e}"),
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,39 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy,
|
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy,
|
||||||
installcoordination_proxy::InstallcoordinationProxy, rsd::RsdHandshake,
|
installcoordination_proxy::InstallcoordinationProxy, provider::IdeviceProvider,
|
||||||
|
rsd::RsdHandshake,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Interact with the RemoteXPC installation coordination proxy")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"info",
|
||||||
|
JkCommand::new()
|
||||||
let matches = Command::new("installationcoordination_proxy")
|
.help("Get info about an app on the device")
|
||||||
.about("")
|
.with_argument(
|
||||||
.arg(
|
JkArgument::new()
|
||||||
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("tunneld")
|
|
||||||
.long("tunneld")
|
|
||||||
.help("Use tunneld")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("info")
|
|
||||||
.about("Get info about an app on the device")
|
|
||||||
.arg(
|
|
||||||
Arg::new("bundle_id")
|
|
||||||
.required(true)
|
.required(true)
|
||||||
.help("The bundle ID to query"),
|
.with_help("The bundle ID to query"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("uninstall")
|
"uninstall",
|
||||||
.about("Get info about an app on the device")
|
JkCommand::new()
|
||||||
.arg(
|
.help("Uninstalls an app on the device")
|
||||||
Arg::new("bundle_id")
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
.required(true)
|
.required(true)
|
||||||
.help("The bundle ID to query"),
|
.with_help("The bundle ID to delete"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
println!("debug_proxy - connect to the debug proxy and run commands");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
|
||||||
let host = matches.get_one::<String>("host");
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
@@ -103,8 +55,12 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("no connect");
|
.expect("no connect");
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("info") {
|
let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
let bundle_id: &String = match matches.get_one("bundle_id") {
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"info" => {
|
||||||
|
let bundle_id: String = match sub_args.next_argument() {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle ID passed");
|
eprintln!("No bundle ID passed");
|
||||||
@@ -112,10 +68,14 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = icp.query_app_path(bundle_id).await.expect("no info");
|
let res = icp
|
||||||
|
.query_app_path(bundle_id.as_str())
|
||||||
|
.await
|
||||||
|
.expect("no info");
|
||||||
println!("Path: {res}");
|
println!("Path: {res}");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("uninstall") {
|
}
|
||||||
let bundle_id: &String = match matches.get_one("bundle_id") {
|
"uninstall" => {
|
||||||
|
let bundle_id: String = match sub_args.next_argument() {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No bundle ID passed");
|
eprintln!("No bundle ID passed");
|
||||||
@@ -123,10 +83,10 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
icp.uninstall_app(bundle_id)
|
icp.uninstall_app(bundle_id.as_str())
|
||||||
.await
|
.await
|
||||||
.expect("uninstall failed");
|
.expect("uninstall failed");
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,65 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// Just lists apps for now
|
// Just lists apps for now
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{
|
||||||
use idevice::{IdeviceService, installation_proxy::InstallationProxyClient};
|
IdeviceService, installation_proxy::InstallationProxyClient, provider::IdeviceProvider,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Manage files in the AFC jail of a device")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"lookup",
|
||||||
|
JkCommand::new().help("Gets the apps on the device"),
|
||||||
let matches = Command::new("core_device_proxy_tun")
|
|
||||||
.about("Start a tunnel")
|
|
||||||
.arg(
|
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("pairing_file")
|
"browse",
|
||||||
.long("pairing-file")
|
JkCommand::new().help("Browses the apps on the device"),
|
||||||
.value_name("PATH")
|
|
||||||
.help("Path to the pairing file"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("udid")
|
"check_capabilities",
|
||||||
.value_name("UDID")
|
JkCommand::new().help("Check the capabilities"),
|
||||||
.help("UDID of the device (overrides host/pairing file)")
|
|
||||||
.index(1),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("about")
|
"install",
|
||||||
.long("about")
|
JkCommand::new()
|
||||||
.help("Show about information")
|
.help("Install an app in the AFC jail")
|
||||||
.action(clap::ArgAction::SetTrue),
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Path in the AFC jail"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("lookup").about("Gets the apps on the device"))
|
.subcommand_required(true)
|
||||||
.subcommand(Command::new("browse").about("Browses the apps on the device"))
|
}
|
||||||
.subcommand(Command::new("check_capabilities").about("Check the capabilities"))
|
|
||||||
.subcommand(
|
|
||||||
Command::new("install")
|
|
||||||
.about("Install an app in the AFC jail")
|
|
||||||
.arg(Arg::new("path")),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!(
|
|
||||||
"instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary."
|
|
||||||
);
|
|
||||||
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, "instproxy-jkcoxson").await
|
|
||||||
{
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut instproxy_client = InstallationProxyClient::connect(&*provider)
|
let mut instproxy_client = InstallationProxyClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to instproxy");
|
.expect("Unable to connect to instproxy");
|
||||||
if matches.subcommand_matches("lookup").is_some() {
|
|
||||||
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("no sub arg");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"lookup" => {
|
||||||
let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
|
let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
|
||||||
for app in apps.keys() {
|
for app in apps.keys() {
|
||||||
println!("{app}");
|
println!("{app}");
|
||||||
}
|
}
|
||||||
} else if matches.subcommand_matches("browse").is_some() {
|
}
|
||||||
|
"browse" => {
|
||||||
instproxy_client.browse(None).await.expect("browse failed");
|
instproxy_client.browse(None).await.expect("browse failed");
|
||||||
} else if matches.subcommand_matches("check_capabilities").is_some() {
|
}
|
||||||
|
"check_capabilities" => {
|
||||||
instproxy_client
|
instproxy_client
|
||||||
.check_capabilities_match(Vec::new(), None)
|
.check_capabilities_match(Vec::new(), None)
|
||||||
.await
|
.await
|
||||||
.expect("check failed");
|
.expect("check failed");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("install") {
|
}
|
||||||
let path: &String = match matches.get_one("path") {
|
"install" => {
|
||||||
|
let path: String = match sub_args.next_argument() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No path passed, pass -h for help");
|
eprintln!("No path passed, pass -h for help");
|
||||||
@@ -102,7 +78,7 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to install")
|
.expect("Failed to install")
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,33 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// Just lists apps for now
|
// Just lists apps for now
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::provider::IdeviceProvider;
|
||||||
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::dvt::location_simulation::LocationSimulationClient;
|
||||||
use idevice::services::simulate_location::LocationSimulationService;
|
use idevice::services::simulate_location::LocationSimulationService;
|
||||||
mod common;
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
#[tokio::main]
|
pub fn register() -> JkCommand {
|
||||||
async fn main() {
|
JkCommand::new()
|
||||||
tracing_subscriber::fmt::init();
|
.help("Simulate device location")
|
||||||
|
.with_subcommand(
|
||||||
|
"clear",
|
||||||
|
JkCommand::new().help("Clears the location set on the device"),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"set",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Set the location on the device")
|
||||||
|
.with_argument(JkArgument::new().with_help("latitude").required(true))
|
||||||
|
.with_argument(JkArgument::new().with_help("longitutde").required(true)),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
let matches = Command::new("simulate_location")
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
.about("Simulate device location")
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No sub arg passed");
|
||||||
.arg(
|
let mut sub_args = sub_args.clone();
|
||||||
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("clear").about("Clears the location set on the device"))
|
|
||||||
.subcommand(
|
|
||||||
Command::new("set")
|
|
||||||
.about("Set the location on the device")
|
|
||||||
.arg(Arg::new("latitude").required(true))
|
|
||||||
.arg(Arg::new("longitude").required(true)),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("simulate_location - Sets the simulated location on an 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, "simulate_location-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
|
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
|
||||||
let rsd_port = proxy.handshake.server_rsd_port;
|
let rsd_port = proxy.handshake.server_rsd_port;
|
||||||
@@ -86,11 +49,13 @@ async fn main() {
|
|||||||
let mut ls_client = LocationSimulationClient::new(&mut ls_client)
|
let mut ls_client = 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() {
|
match sub_name.as_str() {
|
||||||
|
"clear" => {
|
||||||
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") {
|
}
|
||||||
let latitude: &String = match matches.get_one("latitude") {
|
"set" => {
|
||||||
|
let latitude: String = match sub_args.next_argument() {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No latitude passed! Pass -h for help");
|
eprintln!("No latitude passed! Pass -h for help");
|
||||||
@@ -98,7 +63,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
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 sub_args.next_argument() {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No longitude passed! Pass -h for help");
|
eprintln!("No longitude passed! Pass -h for help");
|
||||||
@@ -120,8 +85,8 @@ async fn main() {
|
|||||||
.expect("Failed to set location");
|
.expect("Failed to set location");
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut location_client = match LocationSimulationService::connect(&*provider).await {
|
let mut location_client = match LocationSimulationService::connect(&*provider).await {
|
||||||
@@ -133,11 +98,14 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if matches.subcommand_matches("clear").is_some() {
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"clear" => {
|
||||||
location_client.clear().await.expect("Unable to clear");
|
location_client.clear().await.expect("Unable to clear");
|
||||||
println!("Location cleared!");
|
println!("Location cleared!");
|
||||||
} else if let Some(matches) = matches.subcommand_matches("set") {
|
}
|
||||||
let latitude: &String = match matches.get_one("latitude") {
|
"set" => {
|
||||||
|
let latitude: String = match sub_args.next_argument() {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No latitude passed! Pass -h for help");
|
eprintln!("No latitude passed! Pass -h for help");
|
||||||
@@ -145,7 +113,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let longitude: &String = match matches.get_one("longitude") {
|
let longitude: String = match sub_args.next_argument() {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No longitude passed! Pass -h for help");
|
eprintln!("No longitude passed! Pass -h for help");
|
||||||
@@ -153,15 +121,13 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
location_client
|
location_client
|
||||||
.set(latitude, longitude)
|
.set(latitude.as_str(), longitude.as_str())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to set location");
|
.expect("Failed to set location");
|
||||||
|
|
||||||
println!("Location set!");
|
println!("Location set!");
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +1,79 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command, arg};
|
use idevice::{IdeviceService, lockdown::LockdownClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist};
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
use plist::Value;
|
use plist::Value;
|
||||||
|
use plist_macro::pretty_print_plist;
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Interact with lockdown")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"get",
|
||||||
|
JkCommand::new()
|
||||||
let matches = Command::new("lockdown")
|
.help("Gets a value from lockdown")
|
||||||
.about("Start a tunnel")
|
.with_argument(JkArgument::new().with_help("The value to get")),
|
||||||
.arg(
|
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("pairing_file")
|
"set",
|
||||||
.long("pairing-file")
|
JkCommand::new()
|
||||||
.value_name("PATH")
|
.help("Gets a value from lockdown")
|
||||||
.help("Path to the pairing file"),
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("The value to set")
|
||||||
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("udid")
|
JkArgument::new()
|
||||||
.value_name("UDID")
|
.with_help("The value key to set")
|
||||||
.help("UDID of the device (overrides host/pairing file)")
|
.required(true),
|
||||||
.index(1),
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("about")
|
"recovery",
|
||||||
.long("about")
|
JkCommand::new().help("Tell the device to enter recovery mode"),
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_flag(
|
||||||
Command::new("get")
|
JkFlag::new("domain")
|
||||||
.about("Gets a value")
|
.with_help("The domain to set/get in")
|
||||||
.arg(arg!(-v --value <STRING> "the value to get").required(false))
|
.with_argument(JkArgument::new().required(true)),
|
||||||
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
|
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_flag(JkFlag::new("no-session").with_help("Don't start a TLS session"))
|
||||||
Command::new("set")
|
.subcommand_required(true)
|
||||||
.about("Sets a lockdown value")
|
}
|
||||||
.arg(arg!(-k --key <STRING> "the key to set").required(true))
|
|
||||||
.arg(arg!(-v --value <STRING> "the value to set the key to").required(true))
|
|
||||||
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!(
|
|
||||||
"lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary."
|
|
||||||
);
|
|
||||||
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, "ideviceinfo-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut lockdown_client = LockdownClient::connect(&*provider)
|
let mut lockdown_client = LockdownClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to lockdown");
|
.expect("Unable to connect to lockdown");
|
||||||
|
|
||||||
|
if !arguments.has_flag("no-session") {
|
||||||
lockdown_client
|
lockdown_client
|
||||||
.start_session(&provider.get_pairing_file().await.expect("no pairing file"))
|
.start_session(&provider.get_pairing_file().await.expect("no pairing file"))
|
||||||
.await
|
.await
|
||||||
.expect("no session");
|
.expect("no session");
|
||||||
|
}
|
||||||
|
|
||||||
match matches.subcommand() {
|
let domain: Option<String> = arguments.get_flag("domain");
|
||||||
Some(("get", sub_m)) => {
|
let domain = domain.as_deref();
|
||||||
let key = sub_m.get_one::<String>("value").map(|x| x.as_str());
|
|
||||||
let domain = sub_m.get_one::<String>("domain").map(|x| x.as_str());
|
|
||||||
|
|
||||||
match lockdown_client.get_value(key, domain).await {
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"get" => {
|
||||||
|
let key: Option<String> = sub_args.next_argument();
|
||||||
|
|
||||||
|
match lockdown_client
|
||||||
|
.get_value(
|
||||||
|
match &key {
|
||||||
|
Some(k) => Some(k.as_str()),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
domain,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
println!("{}", pretty_print_plist(&value));
|
println!("{}", pretty_print_plist(&value));
|
||||||
}
|
}
|
||||||
@@ -95,25 +82,21 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"set" => {
|
||||||
Some(("set", sub_m)) => {
|
let value_str: String = sub_args.next_argument().unwrap();
|
||||||
let key = sub_m.get_one::<String>("key").unwrap();
|
let key: String = sub_args.next_argument().unwrap();
|
||||||
let value_str = sub_m.get_one::<String>("value").unwrap();
|
|
||||||
let domain = sub_m.get_one::<String>("domain");
|
|
||||||
|
|
||||||
let value = Value::String(value_str.clone());
|
let value = Value::String(value_str.clone());
|
||||||
|
|
||||||
match lockdown_client
|
match lockdown_client.set_value(key, value, domain).await {
|
||||||
.set_value(key, value, domain.map(|x| x.as_str()))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => println!("Successfully set"),
|
Ok(()) => println!("Successfully set"),
|
||||||
Err(e) => eprintln!("Error setting value: {e}"),
|
Err(e) => eprintln!("Error setting value: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"recovery" => lockdown_client
|
||||||
_ => {
|
.enter_recovery()
|
||||||
eprintln!("No subcommand provided. Try `--help` for usage.");
|
.await
|
||||||
}
|
.expect("Failed to enter recovery"),
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
338
tools/src/main.rs
Normal file
338
tools/src/main.rs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
pairing_file::PairingFile,
|
||||||
|
provider::{IdeviceProvider, TcpProvider},
|
||||||
|
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection, UsbmuxdDevice},
|
||||||
|
};
|
||||||
|
use jkcli::{JkArgument, JkCommand, JkFlag};
|
||||||
|
|
||||||
|
mod activation;
|
||||||
|
mod afc;
|
||||||
|
mod amfi;
|
||||||
|
mod app_service;
|
||||||
|
mod bt_packet_logger;
|
||||||
|
mod companion_proxy;
|
||||||
|
mod crash_logs;
|
||||||
|
mod debug_proxy;
|
||||||
|
mod diagnostics;
|
||||||
|
mod diagnosticsservice;
|
||||||
|
mod dvt_packet_parser;
|
||||||
|
mod heartbeat_client;
|
||||||
|
mod ideviceinfo;
|
||||||
|
mod ideviceinstaller;
|
||||||
|
mod installcoordination_proxy;
|
||||||
|
mod instproxy;
|
||||||
|
mod location_simulation;
|
||||||
|
mod lockdown;
|
||||||
|
mod misagent;
|
||||||
|
mod mobilebackup2;
|
||||||
|
mod mounter;
|
||||||
|
mod notification_proxy_client;
|
||||||
|
mod notifications;
|
||||||
|
mod os_trace_relay;
|
||||||
|
mod pair;
|
||||||
|
mod pcapd;
|
||||||
|
mod preboard;
|
||||||
|
mod process_control;
|
||||||
|
mod remotexpc;
|
||||||
|
mod restore_service;
|
||||||
|
mod screenshot;
|
||||||
|
mod springboardservices;
|
||||||
|
mod syslog_relay;
|
||||||
|
|
||||||
|
mod pcap;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Set the base CLI
|
||||||
|
let arguments = JkCommand::new()
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("about")
|
||||||
|
.with_help("Prints the about message")
|
||||||
|
.with_short_curcuit(|| {
|
||||||
|
eprintln!("idevice-rs-tools - Jackson Coxson\n");
|
||||||
|
eprintln!("Tools to manage and manipulate iOS devices");
|
||||||
|
eprintln!("Version {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
eprintln!("https://github.com/jkcoxson/idevice");
|
||||||
|
eprintln!("\nOn to eternal perfection!");
|
||||||
|
std::process::exit(0);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("version")
|
||||||
|
.with_help("Prints the version")
|
||||||
|
.with_short_curcuit(|| {
|
||||||
|
println!("{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
std::process::exit(0);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("pairing-file")
|
||||||
|
.with_argument(JkArgument::new().required(true))
|
||||||
|
.with_help("The path to the pairing file to use"),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("host")
|
||||||
|
.with_argument(JkArgument::new().required(true))
|
||||||
|
.with_help("The host to connect to"),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("udid")
|
||||||
|
.with_argument(JkArgument::new().required(true))
|
||||||
|
.with_help("The UDID to use"),
|
||||||
|
)
|
||||||
|
.with_subcommand("activation", activation::register())
|
||||||
|
.with_subcommand("afc", afc::register())
|
||||||
|
.with_subcommand("amfi", amfi::register())
|
||||||
|
.with_subcommand("app_service", app_service::register())
|
||||||
|
.with_subcommand("bt_packet_logger", bt_packet_logger::register())
|
||||||
|
.with_subcommand("companion_proxy", companion_proxy::register())
|
||||||
|
.with_subcommand("crash_logs", crash_logs::register())
|
||||||
|
.with_subcommand("debug_proxy", debug_proxy::register())
|
||||||
|
.with_subcommand("diagnostics", diagnostics::register())
|
||||||
|
.with_subcommand("diagnosticsservice", diagnosticsservice::register())
|
||||||
|
.with_subcommand("dvt_packet_parser", dvt_packet_parser::register())
|
||||||
|
.with_subcommand("heartbeat_client", heartbeat_client::register())
|
||||||
|
.with_subcommand("ideviceinfo", ideviceinfo::register())
|
||||||
|
.with_subcommand("ideviceinstaller", ideviceinstaller::register())
|
||||||
|
.with_subcommand(
|
||||||
|
"installcoordination_proxy",
|
||||||
|
installcoordination_proxy::register(),
|
||||||
|
)
|
||||||
|
.with_subcommand("instproxy", instproxy::register())
|
||||||
|
.with_subcommand("location_simulation", location_simulation::register())
|
||||||
|
.with_subcommand("lockdown", lockdown::register())
|
||||||
|
.with_subcommand("misagent", misagent::register())
|
||||||
|
.with_subcommand("mobilebackup2", mobilebackup2::register())
|
||||||
|
.with_subcommand("mounter", mounter::register())
|
||||||
|
.with_subcommand("notifications", notifications::register())
|
||||||
|
.with_subcommand("notification_proxy", notification_proxy_client::register())
|
||||||
|
.with_subcommand("os_trace_relay", os_trace_relay::register())
|
||||||
|
.with_subcommand("pair", pair::register())
|
||||||
|
.with_subcommand("pcapd", pcapd::register())
|
||||||
|
.with_subcommand("preboard", preboard::register())
|
||||||
|
.with_subcommand("process_control", process_control::register())
|
||||||
|
.with_subcommand("remotexpc", remotexpc::register())
|
||||||
|
.with_subcommand("restore_service", restore_service::register())
|
||||||
|
.with_subcommand("screenshot", screenshot::register())
|
||||||
|
.with_subcommand("springboard", springboardservices::register())
|
||||||
|
.with_subcommand("syslog_relay", syslog_relay::register())
|
||||||
|
.subcommand_required(true)
|
||||||
|
.collect()
|
||||||
|
.expect("Failed to collect CLI args");
|
||||||
|
|
||||||
|
let udid = arguments.get_flag::<String>("udid");
|
||||||
|
let host = arguments.get_flag::<String>("host");
|
||||||
|
let pairing_file = arguments.get_flag::<String>("pairing-file");
|
||||||
|
|
||||||
|
let provider = match get_provider(udid, host, pairing_file, "idevice-rs-tools").await {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (subcommand, sub_args) = match arguments.first_subcommand() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
eprintln!("No subcommand passed, pass -h for help");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match subcommand.as_str() {
|
||||||
|
"activation" => {
|
||||||
|
activation::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"afc" => {
|
||||||
|
afc::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"amfi" => {
|
||||||
|
amfi::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"app_service" => {
|
||||||
|
app_service::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"bt_packet_logger" => {
|
||||||
|
bt_packet_logger::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"companion_proxy" => {
|
||||||
|
companion_proxy::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"crash_logs" => {
|
||||||
|
crash_logs::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"debug_proxy" => {
|
||||||
|
debug_proxy::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"diagnostics" => {
|
||||||
|
diagnostics::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"diagnosticsservice" => {
|
||||||
|
diagnosticsservice::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"dvt_packet_parser" => {
|
||||||
|
dvt_packet_parser::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"heartbeat_client" => {
|
||||||
|
heartbeat_client::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"ideviceinfo" => {
|
||||||
|
ideviceinfo::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"ideviceinstaller" => {
|
||||||
|
ideviceinstaller::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"installcoordination_proxy" => {
|
||||||
|
installcoordination_proxy::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"instproxy" => {
|
||||||
|
instproxy::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"location_simulation" => {
|
||||||
|
location_simulation::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"lockdown" => {
|
||||||
|
lockdown::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"misagent" => {
|
||||||
|
misagent::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"mobilebackup2" => {
|
||||||
|
mobilebackup2::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"mounter" => {
|
||||||
|
mounter::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"notifications" => {
|
||||||
|
notifications::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"notification_proxy" => {
|
||||||
|
notification_proxy_client::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"os_trace_relay" => {
|
||||||
|
os_trace_relay::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"pair" => {
|
||||||
|
pair::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"pcapd" => {
|
||||||
|
pcapd::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"preboard" => {
|
||||||
|
preboard::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"process_control" => {
|
||||||
|
process_control::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"remotexpc" => {
|
||||||
|
remotexpc::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"restore_service" => {
|
||||||
|
restore_service::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"screenshot" => {
|
||||||
|
screenshot::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"springboard" => {
|
||||||
|
springboardservices::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
"syslog_relay" => {
|
||||||
|
syslog_relay::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_provider(
|
||||||
|
udid: Option<String>,
|
||||||
|
host: Option<String>,
|
||||||
|
pairing_file: Option<String>,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<Box<dyn IdeviceProvider>, String> {
|
||||||
|
let provider: Box<dyn IdeviceProvider> = if let Some(udid) = udid {
|
||||||
|
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
|
||||||
|
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
|
||||||
|
let socket = tokio::net::TcpStream::connect(socket)
|
||||||
|
.await
|
||||||
|
.expect("unable to connect to socket address");
|
||||||
|
UsbmuxdConnection::new(Box::new(socket), 1)
|
||||||
|
} else {
|
||||||
|
UsbmuxdConnection::default()
|
||||||
|
.await
|
||||||
|
.expect("Unable to connect to usbmxud")
|
||||||
|
};
|
||||||
|
|
||||||
|
let dev = match usbmuxd.get_device(udid.as_str()).await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Device not found: {e:?}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label))
|
||||||
|
} else if let Some(host) = host
|
||||||
|
&& let Some(pairing_file) = pairing_file
|
||||||
|
{
|
||||||
|
let host = match IpAddr::from_str(host.as_str()) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Invalid host: {e:?}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let pairing_file = match PairingFile::read_from_file(pairing_file) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Unable to read pairing file: {e:?}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(TcpProvider {
|
||||||
|
addr: host,
|
||||||
|
pairing_file,
|
||||||
|
label: label.to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
|
||||||
|
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
|
||||||
|
let socket = tokio::net::TcpStream::connect(socket)
|
||||||
|
.await
|
||||||
|
.expect("unable to connect to socket address");
|
||||||
|
UsbmuxdConnection::new(Box::new(socket), 1)
|
||||||
|
} else {
|
||||||
|
UsbmuxdConnection::default()
|
||||||
|
.await
|
||||||
|
.expect("Unable to connect to usbmxud")
|
||||||
|
};
|
||||||
|
let devs = match usbmuxd.get_devices().await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Unable to get devices from usbmuxd: {e:?}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let usb_devs: Vec<&UsbmuxdDevice> = devs
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.connection_type == Connection::Usb)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if devs.is_empty() {
|
||||||
|
return Err("No devices connected!".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let chosen_dev = if !usb_devs.is_empty() {
|
||||||
|
usb_devs[0]
|
||||||
|
} else {
|
||||||
|
&devs[0]
|
||||||
|
};
|
||||||
|
Box::new(chosen_dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label))
|
||||||
|
};
|
||||||
|
Ok(provider)
|
||||||
|
}
|
||||||
@@ -2,85 +2,61 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Arg, Command, arg, value_parser};
|
use idevice::{IdeviceService, misagent::MisagentClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, misagent::MisagentClient};
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Manage provisioning profiles on the device")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"list",
|
||||||
|
JkCommand::new()
|
||||||
let matches = Command::new("core_device_proxy_tun")
|
.help("List profiles installed on the device")
|
||||||
.about("Start a tunnel")
|
.with_argument(
|
||||||
.arg(
|
JkArgument::new()
|
||||||
Arg::new("host")
|
.with_help("Path to save profiles from the device")
|
||||||
.long("host")
|
.required(false),
|
||||||
.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)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("list")
|
|
||||||
.about("Lists the images mounted on the device")
|
|
||||||
.arg(
|
|
||||||
arg!(-s --save <FOLDER> "the folder to save the profiles to")
|
|
||||||
.value_parser(value_parser!(PathBuf)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("remove")
|
"remove",
|
||||||
.about("Remove a provisioning profile")
|
JkCommand::new()
|
||||||
.arg(Arg::new("id").required(true).index(1)),
|
.help("Remove a profile installed on the device")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("ID of the profile to remove")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.with_subcommand(
|
||||||
|
"install",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Install a provisioning profile on the device")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Path to the provisioning profile to install")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
println!(
|
|
||||||
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
|
|
||||||
);
|
|
||||||
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, "misagent-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut misagent_client = MisagentClient::connect(&*provider)
|
let mut misagent_client = MisagentClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to misagent");
|
.expect("Unable to connect to misagent");
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("list") {
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"list" => {
|
||||||
let images = misagent_client
|
let images = misagent_client
|
||||||
.copy_all()
|
.copy_all()
|
||||||
.await
|
.await
|
||||||
.expect("Unable to get images");
|
.expect("Unable to get images");
|
||||||
if let Some(path) = matches.get_one::<PathBuf>("save") {
|
if let Some(path) = sub_args.next_argument::<PathBuf>() {
|
||||||
tokio::fs::create_dir_all(path)
|
tokio::fs::create_dir_all(&path)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to create save DIR");
|
.expect("Unable to create save DIR");
|
||||||
|
|
||||||
@@ -91,10 +67,24 @@ async fn main() {
|
|||||||
.expect("Failed to write image");
|
.expect("Failed to write image");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(matches) = matches.subcommand_matches("remove") {
|
}
|
||||||
let id = matches.get_one::<String>("id").expect("No ID passed");
|
"remove" => {
|
||||||
misagent_client.remove(id).await.expect("Failed to remove");
|
let id = sub_args.next_argument::<String>().expect("No ID passed");
|
||||||
} else {
|
misagent_client
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
.remove(id.as_str())
|
||||||
|
.await
|
||||||
|
.expect("Failed to remove");
|
||||||
|
}
|
||||||
|
"install" => {
|
||||||
|
let path = sub_args
|
||||||
|
.next_argument::<PathBuf>()
|
||||||
|
.expect("No profile path passed");
|
||||||
|
let profile = tokio::fs::read(path).await.expect("Unable to read profile");
|
||||||
|
misagent_client
|
||||||
|
.install(profile)
|
||||||
|
.await
|
||||||
|
.expect("Failed to install profile");
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,191 +1,135 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// Mobile Backup 2 tool for iOS devices
|
// Mobile Backup 2 tool for iOS devices
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService,
|
IdeviceService,
|
||||||
mobilebackup2::{MobileBackup2Client, RestoreOptions},
|
mobilebackup2::{MobileBackup2Client, RestoreOptions},
|
||||||
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
use plist::Dictionary;
|
use plist::Dictionary;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Mobile Backup 2 tool for iOS devices")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"info",
|
||||||
|
JkCommand::new()
|
||||||
let matches = Command::new("mobilebackup2")
|
.help("Get backup information from a local backup directory")
|
||||||
.about("Mobile Backup 2 tool for iOS devices")
|
.with_argument(
|
||||||
.arg(
|
JkArgument::new()
|
||||||
Arg::new("host")
|
.with_help("Backup DIR to read from")
|
||||||
.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("info")
|
|
||||||
.about("Get backup information from a local backup directory")
|
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
|
||||||
.arg(
|
|
||||||
Arg::new("source")
|
|
||||||
.long("source")
|
|
||||||
.value_name("SOURCE")
|
|
||||||
.help("Source identifier (defaults to current UDID)"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("list")
|
|
||||||
.about("List files of the last backup from a local backup directory")
|
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
|
||||||
.arg(Arg::new("source").long("source").value_name("SOURCE")),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new("backup")
|
|
||||||
.about("Start a backup operation")
|
|
||||||
.arg(
|
|
||||||
Arg::new("dir")
|
|
||||||
.long("dir")
|
|
||||||
.value_name("DIR")
|
|
||||||
.help("Backup directory on host")
|
|
||||||
.required(true),
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("target")
|
JkArgument::new()
|
||||||
.long("target")
|
.with_help("Source identifier (defaults to current UDID)")
|
||||||
.value_name("TARGET")
|
.required(true),
|
||||||
.help("Target identifier for the backup"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("source")
|
|
||||||
.long("source")
|
|
||||||
.value_name("SOURCE")
|
|
||||||
.help("Source identifier for the backup"),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("restore")
|
"list",
|
||||||
.about("Restore from a local backup directory (DeviceLink)")
|
JkCommand::new()
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
.help("List files of the last backup from a local backup directory")
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("source")
|
JkArgument::new()
|
||||||
.long("source")
|
.with_help("Backup DIR to read from")
|
||||||
.value_name("SOURCE")
|
.required(true),
|
||||||
.help("Source UDID; defaults to current device UDID"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("password")
|
JkArgument::new()
|
||||||
.long("password")
|
.with_help("Source identifier (defaults to current UDID)")
|
||||||
.value_name("PWD")
|
.required(true),
|
||||||
.help("Backup password if encrypted"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-reboot")
|
|
||||||
.long("no-reboot")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-copy")
|
|
||||||
.long("no-copy")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("no-settings")
|
|
||||||
.long("no-settings")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("system")
|
|
||||||
.long("system")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("remove")
|
|
||||||
.long("remove")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("unback")
|
"backup",
|
||||||
.about("Unpack a complete backup to device hierarchy")
|
JkCommand::new()
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
.help("Start a backup operation")
|
||||||
.arg(Arg::new("source").long("source").value_name("SOURCE"))
|
.with_argument(
|
||||||
.arg(Arg::new("password").long("password").value_name("PWD")),
|
JkArgument::new()
|
||||||
)
|
.with_help("Backup directory on host")
|
||||||
.subcommand(
|
|
||||||
Command::new("extract")
|
|
||||||
.about("Extract a file from a previous backup")
|
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
|
||||||
.arg(Arg::new("source").long("source").value_name("SOURCE"))
|
|
||||||
.arg(
|
|
||||||
Arg::new("domain")
|
|
||||||
.long("domain")
|
|
||||||
.value_name("DOMAIN")
|
|
||||||
.required(true),
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_argument(
|
||||||
Arg::new("path")
|
JkArgument::new()
|
||||||
.long("path")
|
.with_help("Target identifier for the backup")
|
||||||
.value_name("REL_PATH")
|
|
||||||
.required(true),
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(Arg::new("password").long("password").value_name("PWD")),
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Source identifier for the backup")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_subcommand(
|
||||||
Command::new("change-password")
|
"restore",
|
||||||
.about("Change backup password")
|
JkCommand::new()
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
|
.help("Restore from a local backup directory (DeviceLink)")
|
||||||
.arg(Arg::new("old").long("old").value_name("OLD"))
|
.with_argument(JkArgument::new().with_help("DIR").required(true))
|
||||||
.arg(Arg::new("new").long("new").value_name("NEW")),
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Source UDID; defaults to current device UDID")
|
||||||
|
.required(true),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.with_argument(
|
||||||
Command::new("erase-device")
|
JkArgument::new()
|
||||||
.about("Erase the device via mobilebackup2")
|
.with_help("Backup password if encrypted")
|
||||||
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)),
|
.required(true),
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("freespace").about("Get free space information"))
|
.with_flag(JkFlag::new("no-reboot"))
|
||||||
.subcommand(Command::new("encryption").about("Check backup encryption status"))
|
.with_flag(JkFlag::new("no-copy"))
|
||||||
.get_matches();
|
.with_flag(JkFlag::new("no-settings"))
|
||||||
|
.with_flag(JkFlag::new("system"))
|
||||||
if matches.get_flag("about") {
|
.with_flag(JkFlag::new("remove")),
|
||||||
println!("mobilebackup2 - manage device backups using Mobile Backup 2 service");
|
)
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
.with_subcommand(
|
||||||
return;
|
"unback",
|
||||||
}
|
JkCommand::new()
|
||||||
|
.help("Unpack a complete backup to device hierarchy")
|
||||||
let udid = matches.get_one::<String>("udid");
|
.with_argument(JkArgument::new().with_help("DIR").required(true))
|
||||||
let host = matches.get_one::<String>("host");
|
.with_argument(JkArgument::new().with_help("Source"))
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
.with_argument(JkArgument::new().with_help("Password")),
|
||||||
|
)
|
||||||
let provider =
|
.with_subcommand(
|
||||||
match common::get_provider(udid, host, pairing_file, "mobilebackup2-jkcoxson").await {
|
"extract",
|
||||||
Ok(p) => p,
|
JkCommand::new()
|
||||||
Err(e) => {
|
.help("Extract a file from a previous backup")
|
||||||
eprintln!("Error creating provider: {e}");
|
.with_argument(JkArgument::new().with_help("DIR").required(true))
|
||||||
return;
|
.with_argument(JkArgument::new().with_help("Source").required(true))
|
||||||
}
|
.with_argument(JkArgument::new().with_help("Domain").required(true))
|
||||||
};
|
.with_argument(JkArgument::new().with_help("Path").required(true))
|
||||||
|
.with_argument(JkArgument::new().with_help("Password").required(true)),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"change-password",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Change backup password")
|
||||||
|
.with_argument(JkArgument::new().with_help("DIR").required(true))
|
||||||
|
.with_argument(JkArgument::new().with_help("Old password").required(true))
|
||||||
|
.with_argument(JkArgument::new().with_help("New password").required(true)),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"erase-device",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Erase the device via mobilebackup2")
|
||||||
|
.with_argument(JkArgument::new().with_help("DIR").required(true)),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"freespace",
|
||||||
|
JkCommand::new().help("Get free space information"),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"encryption",
|
||||||
|
JkCommand::new().help("Check backup encryption status"),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
|
let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -194,11 +138,16 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match matches.subcommand() {
|
let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
Some(("info", sub)) => {
|
let mut sub_args = sub_args.clone();
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
|
||||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
match sub_name.as_str() {
|
||||||
match backup_client.info_from_path(Path::new(dir), source).await {
|
"info" => {
|
||||||
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
|
let source = sub_args.next_argument::<String>();
|
||||||
|
let source = source.as_deref();
|
||||||
|
|
||||||
|
match backup_client.info_from_path(Path::new(&dir), source).await {
|
||||||
Ok(dict) => {
|
Ok(dict) => {
|
||||||
println!("Backup Information:");
|
println!("Backup Information:");
|
||||||
for (k, v) in dict {
|
for (k, v) in dict {
|
||||||
@@ -208,10 +157,12 @@ async fn main() {
|
|||||||
Err(e) => eprintln!("Failed to get info: {e}"),
|
Err(e) => eprintln!("Failed to get info: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("list", sub)) => {
|
"list" => {
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
let source = sub_args.next_argument::<String>();
|
||||||
match backup_client.list_from_path(Path::new(dir), source).await {
|
let source = source.as_deref();
|
||||||
|
|
||||||
|
match backup_client.list_from_path(Path::new(&dir), source).await {
|
||||||
Ok(dict) => {
|
Ok(dict) => {
|
||||||
println!("List Response:");
|
println!("List Response:");
|
||||||
for (k, v) in dict {
|
for (k, v) in dict {
|
||||||
@@ -221,12 +172,12 @@ async fn main() {
|
|||||||
Err(e) => eprintln!("Failed to list: {e}"),
|
Err(e) => eprintln!("Failed to list: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("backup", sub_matches)) => {
|
"backup" => {
|
||||||
let target = sub_matches.get_one::<String>("target").map(|s| s.as_str());
|
let target = sub_args.next_argument::<String>();
|
||||||
let source = sub_matches.get_one::<String>("source").map(|s| s.as_str());
|
let target = target.as_deref();
|
||||||
let dir = sub_matches
|
let source = sub_args.next_argument::<String>();
|
||||||
.get_one::<String>("dir")
|
let source = source.as_deref();
|
||||||
.expect("dir is required");
|
let dir = sub_args.next_argument::<String>().expect("dir is required");
|
||||||
|
|
||||||
println!("Starting backup operation...");
|
println!("Starting backup operation...");
|
||||||
let res = backup_client
|
let res = backup_client
|
||||||
@@ -234,95 +185,112 @@ async fn main() {
|
|||||||
.await;
|
.await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
eprintln!("Failed to send backup request: {e}");
|
eprintln!("Failed to send backup request: {e}");
|
||||||
} else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await {
|
} else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(&dir)).await {
|
||||||
eprintln!("Backup failed during DL loop: {e}");
|
eprintln!("Backup failed during DL loop: {e}");
|
||||||
} else {
|
} else {
|
||||||
println!("Backup flow finished");
|
println!("Backup flow finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("restore", sub)) => {
|
"restore" => {
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
let source = sub_args.next_argument::<String>();
|
||||||
|
let source = source.as_deref();
|
||||||
|
|
||||||
let mut ropts = RestoreOptions::new();
|
let mut ropts = RestoreOptions::new();
|
||||||
if sub.get_flag("no-reboot") {
|
if sub_args.has_flag("no-reboot") {
|
||||||
ropts = ropts.with_reboot(false);
|
ropts = ropts.with_reboot(false);
|
||||||
}
|
}
|
||||||
if sub.get_flag("no-copy") {
|
if sub_args.has_flag("no-copy") {
|
||||||
ropts = ropts.with_copy(false);
|
ropts = ropts.with_copy(false);
|
||||||
}
|
}
|
||||||
if sub.get_flag("no-settings") {
|
if sub_args.has_flag("no-settings") {
|
||||||
ropts = ropts.with_preserve_settings(false);
|
ropts = ropts.with_preserve_settings(false);
|
||||||
}
|
}
|
||||||
if sub.get_flag("system") {
|
if sub_args.has_flag("system") {
|
||||||
ropts = ropts.with_system_files(true);
|
ropts = ropts.with_system_files(true);
|
||||||
}
|
}
|
||||||
if sub.get_flag("remove") {
|
if sub_args.has_flag("remove") {
|
||||||
ropts = ropts.with_remove_items_not_restored(true);
|
ropts = ropts.with_remove_items_not_restored(true);
|
||||||
}
|
}
|
||||||
if let Some(pw) = sub.get_one::<String>("password") {
|
if let Some(pw) = sub_args.next_argument::<String>() {
|
||||||
ropts = ropts.with_password(pw);
|
ropts = ropts.with_password(pw);
|
||||||
}
|
}
|
||||||
match backup_client
|
match backup_client
|
||||||
.restore_from_path(Path::new(dir), source, Some(ropts))
|
.restore_from_path(Path::new(&dir), source, Some(ropts))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => println!("Restore flow finished"),
|
Ok(_) => println!("Restore flow finished"),
|
||||||
Err(e) => eprintln!("Restore failed: {e}"),
|
Err(e) => eprintln!("Restore failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("unback", sub)) => {
|
"unback" => {
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
let source = sub_args.next_argument::<String>();
|
||||||
let password = sub.get_one::<String>("password").map(|s| s.as_str());
|
let source = source.as_deref();
|
||||||
|
let password = sub_args.next_argument::<String>();
|
||||||
|
let password = password.as_deref();
|
||||||
|
|
||||||
match backup_client
|
match backup_client
|
||||||
.unback_from_path(Path::new(dir), password, source)
|
.unback_from_path(Path::new(&dir), password, source)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => println!("Unback finished"),
|
Ok(_) => println!("Unback finished"),
|
||||||
Err(e) => eprintln!("Unback failed: {e}"),
|
Err(e) => eprintln!("Unback failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("extract", sub)) => {
|
"extract" => {
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
let source = sub.get_one::<String>("source").map(|s| s.as_str());
|
let source = sub_args.next_argument::<String>();
|
||||||
let domain = sub.get_one::<String>("domain").unwrap();
|
let source = source.as_deref();
|
||||||
let rel = sub.get_one::<String>("path").unwrap();
|
let domain = sub_args.next_argument::<String>().unwrap();
|
||||||
let password = sub.get_one::<String>("password").map(|s| s.as_str());
|
let rel = sub_args.next_argument::<String>().unwrap();
|
||||||
|
let password = sub_args.next_argument::<String>();
|
||||||
|
let password = password.as_deref();
|
||||||
|
|
||||||
match backup_client
|
match backup_client
|
||||||
.extract_from_path(domain, rel, Path::new(dir), password, source)
|
.extract_from_path(
|
||||||
|
domain.as_str(),
|
||||||
|
rel.as_str(),
|
||||||
|
Path::new(&dir),
|
||||||
|
password,
|
||||||
|
source,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => println!("Extract finished"),
|
Ok(_) => println!("Extract finished"),
|
||||||
Err(e) => eprintln!("Extract failed: {e}"),
|
Err(e) => eprintln!("Extract failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("change-password", sub)) => {
|
"change-password" => {
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
let old = sub.get_one::<String>("old").map(|s| s.as_str());
|
let old = sub_args.next_argument::<String>();
|
||||||
let newv = sub.get_one::<String>("new").map(|s| s.as_str());
|
let old = old.as_deref();
|
||||||
|
let newv = sub_args.next_argument::<String>();
|
||||||
|
let newv = newv.as_deref();
|
||||||
|
|
||||||
match backup_client
|
match backup_client
|
||||||
.change_password_from_path(Path::new(dir), old, newv)
|
.change_password_from_path(Path::new(&dir), old, newv)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => println!("Change password finished"),
|
Ok(_) => println!("Change password finished"),
|
||||||
Err(e) => eprintln!("Change password failed: {e}"),
|
Err(e) => eprintln!("Change password failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("erase-device", sub)) => {
|
"erase-device" => {
|
||||||
let dir = sub.get_one::<String>("dir").unwrap();
|
let dir = sub_args.next_argument::<String>().unwrap();
|
||||||
match backup_client.erase_device_from_path(Path::new(dir)).await {
|
match backup_client.erase_device_from_path(Path::new(&dir)).await {
|
||||||
Ok(_) => println!("Erase device command sent"),
|
Ok(_) => println!("Erase device command sent"),
|
||||||
Err(e) => eprintln!("Erase device failed: {e}"),
|
Err(e) => eprintln!("Erase device failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(("freespace", _)) => match backup_client.get_freespace().await {
|
"freespace" => match backup_client.get_freespace().await {
|
||||||
Ok(freespace) => {
|
Ok(freespace) => {
|
||||||
let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0);
|
let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||||
println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)");
|
println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)");
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("Failed to get free space: {e}"),
|
Err(e) => eprintln!("Failed to get free space: {e}"),
|
||||||
},
|
},
|
||||||
Some(("encryption", _)) => match backup_client.check_backup_encryption().await {
|
"encryption" => match backup_client.check_backup_encryption().await {
|
||||||
Ok(is_encrypted) => {
|
Ok(is_encrypted) => {
|
||||||
println!(
|
println!(
|
||||||
"Backup encryption: {}",
|
"Backup encryption: {}",
|
||||||
|
|||||||
@@ -3,90 +3,62 @@
|
|||||||
|
|
||||||
use std::{io::Write, path::PathBuf};
|
use std::{io::Write, path::PathBuf};
|
||||||
|
|
||||||
use clap::{Arg, Command, arg, value_parser};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter,
|
IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter,
|
||||||
pretty_print_plist,
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
|
use plist_macro::pretty_print_plist;
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Manage mounts on an iOS device")
|
||||||
async fn main() {
|
.with_subcommand(
|
||||||
tracing_subscriber::fmt::init();
|
"list",
|
||||||
|
JkCommand::new().help("Lists the images mounted on the device"),
|
||||||
let matches = Command::new("core_device_proxy_tun")
|
|
||||||
.about("Start a tunnel")
|
|
||||||
.arg(
|
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("pairing_file")
|
"lookup",
|
||||||
.long("pairing-file")
|
JkCommand::new().help("Lookup the image signature on the device"),
|
||||||
.value_name("PATH")
|
|
||||||
.help("Path to the pairing file"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("udid")
|
"unmount",
|
||||||
.value_name("UDID")
|
JkCommand::new().help("Unmounts the developer disk image"),
|
||||||
.help("UDID of the device (overrides host/pairing file)")
|
|
||||||
.index(1),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("about")
|
"mount",
|
||||||
.long("about")
|
JkCommand::new()
|
||||||
.help("Show about information")
|
.help("Mounts the developer disk image")
|
||||||
.action(clap::ArgAction::SetTrue),
|
.with_flag(
|
||||||
)
|
JkFlag::new("image")
|
||||||
.subcommand(Command::new("list").about("Lists the images mounted on the device"))
|
.with_short("i")
|
||||||
.subcommand(Command::new("unmount").about("Unmounts the developer disk image"))
|
.with_argument(JkArgument::new().required(true))
|
||||||
.subcommand(
|
.with_help("A path to the image to mount")
|
||||||
Command::new("mount")
|
|
||||||
.about("Mounts the developer disk image")
|
|
||||||
.arg(
|
|
||||||
arg!(-i --image <FILE> "the developer disk image to mount")
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
|
||||||
.required(true),
|
.required(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_flag(
|
||||||
arg!(-b --manifest <FILE> "the build manifest (iOS 17+)")
|
JkFlag::new("manifest")
|
||||||
.value_parser(value_parser!(PathBuf)),
|
.with_short("b")
|
||||||
|
.with_argument(JkArgument::new())
|
||||||
|
.with_help("the build manifest (iOS 17+)"),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_flag(
|
||||||
arg!(-t --trustcache <FILE> "the trust cache (iOS 17+)")
|
JkFlag::new("trustcache")
|
||||||
.value_parser(value_parser!(PathBuf)),
|
.with_short("t")
|
||||||
|
.with_argument(JkArgument::new())
|
||||||
|
.with_help("the trust cache (iOS 17+)"),
|
||||||
)
|
)
|
||||||
.arg(
|
.with_flag(
|
||||||
arg!(-s --signature <FILE> "the image signature (iOS < 17.0")
|
JkFlag::new("signature")
|
||||||
.value_parser(value_parser!(PathBuf)),
|
.with_short("s")
|
||||||
|
.with_argument(JkArgument::new())
|
||||||
|
.with_help("the image signature (iOS < 17.0"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!(
|
|
||||||
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
|
|
||||||
);
|
|
||||||
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, "ideviceinfo-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut lockdown_client = LockdownClient::connect(&*provider)
|
let mut lockdown_client = LockdownClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to lockdown");
|
.expect("Unable to connect to lockdown");
|
||||||
@@ -119,7 +91,12 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Unable to connect to image mounter");
|
.expect("Unable to connect to image mounter");
|
||||||
|
|
||||||
if matches.subcommand_matches("list").is_some() {
|
let (subcommand, sub_args) = arguments
|
||||||
|
.first_subcommand()
|
||||||
|
.expect("No subcommand passed! Pass -h for help");
|
||||||
|
|
||||||
|
match subcommand.as_str() {
|
||||||
|
"list" => {
|
||||||
let images = mounter_client
|
let images = mounter_client
|
||||||
.copy_devices()
|
.copy_devices()
|
||||||
.await
|
.await
|
||||||
@@ -127,7 +104,19 @@ async fn main() {
|
|||||||
for i in images {
|
for i in images {
|
||||||
println!("{}", pretty_print_plist(&i));
|
println!("{}", pretty_print_plist(&i));
|
||||||
}
|
}
|
||||||
} else if matches.subcommand_matches("unmount").is_some() {
|
}
|
||||||
|
"lookup" => {
|
||||||
|
let sig = mounter_client
|
||||||
|
.lookup_image(if product_version < 17 {
|
||||||
|
"Developer"
|
||||||
|
} else {
|
||||||
|
"Personalized"
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to lookup images");
|
||||||
|
println!("Image signature: {sig:02X?}");
|
||||||
|
}
|
||||||
|
"unmount" => {
|
||||||
if product_version < 17 {
|
if product_version < 17 {
|
||||||
mounter_client
|
mounter_client
|
||||||
.unmount_image("/Developer")
|
.unmount_image("/Developer")
|
||||||
@@ -139,8 +128,9 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to unmount");
|
.expect("Failed to unmount");
|
||||||
}
|
}
|
||||||
} else if let Some(matches) = matches.subcommand_matches("mount") {
|
}
|
||||||
let image: &PathBuf = match matches.get_one("image") {
|
"mount" => {
|
||||||
|
let image: PathBuf = match sub_args.get_flag("image") {
|
||||||
Some(i) => i,
|
Some(i) => i,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No image was passed! Pass -h for help");
|
eprintln!("No image was passed! Pass -h for help");
|
||||||
@@ -149,7 +139,7 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
let image = tokio::fs::read(image).await.expect("Unable to read image");
|
let image = tokio::fs::read(image).await.expect("Unable to read image");
|
||||||
if product_version < 17 {
|
if product_version < 17 {
|
||||||
let signature: &PathBuf = match matches.get_one("signature") {
|
let signature: PathBuf = match sub_args.get_flag("signature") {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No signature was passed! Pass -h for help");
|
eprintln!("No signature was passed! Pass -h for help");
|
||||||
@@ -165,7 +155,7 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Unable to mount");
|
.expect("Unable to mount");
|
||||||
} else {
|
} else {
|
||||||
let manifest: &PathBuf = match matches.get_one("manifest") {
|
let manifest: PathBuf = match sub_args.get_flag("manifest") {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No build manifest was passed! Pass -h for help");
|
eprintln!("No build manifest was passed! Pass -h for help");
|
||||||
@@ -176,7 +166,7 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Unable to read signature");
|
.expect("Unable to read signature");
|
||||||
|
|
||||||
let trust_cache: &PathBuf = match matches.get_one("trustcache") {
|
let trust_cache: PathBuf = match sub_args.get_flag("trustcache") {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("No trust cache was passed! Pass -h for help");
|
eprintln!("No trust cache was passed! Pass -h for help");
|
||||||
@@ -225,8 +215,7 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Unable to mount");
|
.expect("Unable to mount");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
|
||||||
}
|
}
|
||||||
return;
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
tools/src/notification_proxy_client.rs
Normal file
85
tools/src/notification_proxy_client.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceService, notification_proxy::NotificationProxyClient, provider::IdeviceProvider,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Notification proxy")
|
||||||
|
.with_subcommand(
|
||||||
|
"observe",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Observe notifications from the device")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("The notification ID to observe")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"post",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Post a notification to the device")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("The notification ID to post")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
|
let mut client = NotificationProxyClient::connect(&*provider)
|
||||||
|
.await
|
||||||
|
.expect("Unable to connect to notification proxy");
|
||||||
|
|
||||||
|
let (subcommand, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match subcommand.as_str() {
|
||||||
|
"observe" => {
|
||||||
|
let input: String = sub_args
|
||||||
|
.next_argument::<String>()
|
||||||
|
.expect("No notification ID passed");
|
||||||
|
|
||||||
|
let notifications: Vec<&str> = input.split_whitespace().collect();
|
||||||
|
client
|
||||||
|
.observe_notifications(¬ifications)
|
||||||
|
.await
|
||||||
|
.expect("Failed to observe notifications");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
println!("\nShutdown signal received, exiting.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.receive_notification() => {
|
||||||
|
match result {
|
||||||
|
Ok(notif) => println!("Received notification: {}", notif),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to receive notification: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"post" => {
|
||||||
|
let notification: String = sub_args
|
||||||
|
.next_argument::<String>()
|
||||||
|
.expect("No notification ID passed");
|
||||||
|
|
||||||
|
client
|
||||||
|
.post_notification(¬ification)
|
||||||
|
.await
|
||||||
|
.expect("Failed to post notification");
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +1,16 @@
|
|||||||
// Monitor memory and app notifications
|
// Monitor memory and app notifications
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{
|
||||||
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
|
||||||
mod common;
|
rsd::RsdHandshake,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
#[tokio::main]
|
pub fn register() -> JkCommand {
|
||||||
async fn main() {
|
JkCommand::new().help("Notification proxy")
|
||||||
tracing_subscriber::fmt::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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
@@ -80,7 +37,6 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to start notifications");
|
.expect("Failed to start notifications");
|
||||||
|
|
||||||
// Handle Ctrl+C gracefully
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = tokio::signal::ctrl_c() => {
|
_ = tokio::signal::ctrl_c() => {
|
||||||
@@ -88,7 +44,6 @@ async fn main() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branch 2: Wait for the next batch of notifications.
|
|
||||||
result = notification_client.get_notification() => {
|
result = notification_client.get_notification() => {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
eprintln!("Failed to get notifications: {}", e);
|
eprintln!("Failed to get notifications: {}", e);
|
||||||
|
|||||||
@@ -1,58 +1,13 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient};
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("Relay OS logs")
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("os_trace_relay")
|
|
||||||
.about("Relay system logs")
|
|
||||||
.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)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("Relay logs on 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, "misagent-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let log_client = OsTraceRelayClient::connect(&*provider)
|
let log_client = OsTraceRelayClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to misagent");
|
.expect("Unable to connect to misagent");
|
||||||
|
|||||||
@@ -1,46 +1,35 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService,
|
IdeviceService,
|
||||||
lockdown::LockdownClient,
|
lockdown::LockdownClient,
|
||||||
|
provider::IdeviceProvider,
|
||||||
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection},
|
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection},
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
|
|
||||||
#[tokio::main]
|
pub fn register() -> JkCommand {
|
||||||
async fn main() {
|
JkCommand::new()
|
||||||
tracing_subscriber::fmt::init();
|
.help("Manage files in the AFC jail of a device")
|
||||||
|
.with_argument(JkArgument::new().with_help("A UDID to override and pair with"))
|
||||||
let matches = Command::new("pair")
|
.with_flag(
|
||||||
.about("Pair with the device")
|
JkFlag::new("name")
|
||||||
.arg(
|
.with_help("The host name to report to the device")
|
||||||
Arg::new("udid")
|
.with_argument(JkArgument::new().required(true))
|
||||||
.value_name("UDID")
|
.with_short("n"),
|
||||||
.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") {
|
pub async fn main(arguments: &CollectedArguments, _provider: Box<dyn IdeviceProvider>) {
|
||||||
println!("pair - pair with the device");
|
let mut arguments = arguments.clone();
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
let udid: Option<String> = arguments.next_argument();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
|
|
||||||
let mut u = UsbmuxdConnection::default()
|
let mut u = UsbmuxdConnection::default()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to connect to usbmuxd");
|
.expect("Failed to connect to usbmuxd");
|
||||||
let dev = match udid {
|
let dev = match udid {
|
||||||
Some(udid) => u
|
Some(udid) => u
|
||||||
.get_device(udid)
|
.get_device(udid.as_str())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get device with specific udid"),
|
.expect("Failed to get device with specific udid"),
|
||||||
None => u
|
None => u
|
||||||
@@ -62,8 +51,11 @@ async fn main() {
|
|||||||
};
|
};
|
||||||
let id = uuid::Uuid::new_v4().to_string().to_uppercase();
|
let id = uuid::Uuid::new_v4().to_string().to_uppercase();
|
||||||
|
|
||||||
|
let name = arguments.get_flag::<String>("name");
|
||||||
|
let name = name.as_deref();
|
||||||
|
|
||||||
let mut pairing_file = lockdown_client
|
let mut pairing_file = lockdown_client
|
||||||
.pair(id, u.get_buid().await.unwrap())
|
.pair(id, u.get_buid().await.unwrap(), name)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to pair");
|
.expect("Failed to pair");
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,20 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService,
|
IdeviceService,
|
||||||
pcapd::{PcapFileWriter, PcapdClient},
|
pcapd::{PcapFileWriter, PcapdClient},
|
||||||
|
provider::IdeviceProvider,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Writes pcap network data")
|
||||||
|
.with_argument(JkArgument::new().with_help("Write PCAP to this file (use '-' for stdout)"))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
let out = arguments.clone().next_argument::<String>();
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("pcapd")
|
|
||||||
.about("Capture IP packets")
|
|
||||||
.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),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("out")
|
|
||||||
.long("out")
|
|
||||||
.value_name("PCAP")
|
|
||||||
.help("Write PCAP to this file (use '-' for stdout)"),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("bt_packet_logger - capture bluetooth packets");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let out = matches.get_one::<String>("out").map(String::to_owned);
|
|
||||||
|
|
||||||
let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut logger_client = PcapdClient::connect(&*provider)
|
let mut logger_client = PcapdClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,76 +1,34 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{IdeviceService, preboard_service::PreboardServiceClient, provider::IdeviceProvider};
|
||||||
use idevice::{IdeviceService, preboard_service::PreboardServiceClient};
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Interact with the preboard service")
|
||||||
async fn main() {
|
.with_subcommand("create", JkCommand::new().help("Create a stashbag??"))
|
||||||
tracing_subscriber::fmt::init();
|
.with_subcommand("commit", JkCommand::new().help("Commit a stashbag??"))
|
||||||
|
.subcommand_required(true)
|
||||||
let matches = Command::new("preboard")
|
}
|
||||||
.about("Mess with developer mode")
|
|
||||||
.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("create").about("Create a stashbag??"))
|
|
||||||
.subcommand(Command::new("commit").about("Commit a stashbag??"))
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("preboard - no idea what this does");
|
|
||||||
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, "amfi-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let mut pc = PreboardServiceClient::connect(&*provider)
|
let mut pc = PreboardServiceClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to connect to Preboard");
|
.expect("Failed to connect to Preboard");
|
||||||
|
|
||||||
if matches.subcommand_matches("create").is_some() {
|
let (sub_name, _) = arguments.first_subcommand().unwrap();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"create" => {
|
||||||
pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
|
pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create");
|
.expect("Failed to create");
|
||||||
} else if matches.subcommand_matches("commit").is_some() {
|
}
|
||||||
|
"commit" => {
|
||||||
pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
|
pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create");
|
.expect("Failed to create");
|
||||||
} else {
|
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
|
||||||
}
|
}
|
||||||
return;
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,26 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::provider::IdeviceProvider;
|
||||||
use idevice::services::lockdown::LockdownClient;
|
use idevice::services::lockdown::LockdownClient;
|
||||||
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Launch an app with process control")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("The bundle ID to launch"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let matches = Command::new("process_control")
|
let mut arguments = arguments.clone();
|
||||||
.about("Query process control")
|
|
||||||
.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(2),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("tunneld")
|
|
||||||
.long("tunneld")
|
|
||||||
.help("Use tunneld for connection")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("bundle_id")
|
|
||||||
.value_name("Bundle ID")
|
|
||||||
.help("Bundle ID of the app to launch")
|
|
||||||
.index(1),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
let bundle_id: String = arguments.next_argument().expect("No bundle ID specified");
|
||||||
println!("process_control - launch and manage processes on the device");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
|
||||||
let host = matches.get_one::<String>("host");
|
|
||||||
let bundle_id = matches
|
|
||||||
.get_one::<String>("bundle_id")
|
|
||||||
.expect("No bundle ID specified");
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
match common::get_provider(udid, host, pairing_file, "process_control-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rs_client_opt: Option<
|
let mut rs_client_opt: Option<
|
||||||
idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>,
|
idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>,
|
||||||
|
|||||||
@@ -1,65 +1,17 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
// Print out all the RemoteXPC services
|
// Print out all the RemoteXPC services
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake,
|
IdeviceService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
|
||||||
tcp::stream::AdapterStream,
|
rsd::RsdHandshake, tcp::stream::AdapterStream,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("Get services from RemoteXPC")
|
||||||
#[tokio::main]
|
}
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("remotexpc")
|
|
||||||
.about("Get services from RemoteXPC")
|
|
||||||
.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") {
|
|
||||||
println!("remotexpc - get info from RemoteXPC");
|
|
||||||
println!("Copyright (c) 2025 Jackson Coxson");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let udid = matches.get_one::<String>("udid");
|
|
||||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
|
||||||
let host = matches.get_one::<String>("host");
|
|
||||||
|
|
||||||
let provider = match common::get_provider(udid, host, pairing_file, "remotexpc-jkcoxson").await
|
|
||||||
{
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
|
|||||||
@@ -1,76 +1,41 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
use idevice::{
|
use idevice::{
|
||||||
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary,
|
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
|
||||||
restore_service::RestoreServiceClient, rsd::RsdHandshake,
|
restore_service::RestoreServiceClient, rsd::RsdHandshake,
|
||||||
};
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
|
use plist_macro::pretty_print_dictionary;
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
#[tokio::main]
|
.help("Interact with the Restore Service service")
|
||||||
async fn main() {
|
.with_subcommand("delay", JkCommand::new().help("Delay recovery image"))
|
||||||
tracing_subscriber::fmt::init();
|
.with_subcommand("recovery", JkCommand::new().help("Enter recovery mode"))
|
||||||
|
.with_subcommand("reboot", JkCommand::new().help("Reboots the device"))
|
||||||
let matches = Command::new("restore_service")
|
.with_subcommand(
|
||||||
.about("Interact with the Restore Service service")
|
"preflightinfo",
|
||||||
.arg(
|
JkCommand::new().help("Gets the preflight info"),
|
||||||
Arg::new("host")
|
|
||||||
.long("host")
|
|
||||||
.value_name("HOST")
|
|
||||||
.help("IP address of the device"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand("nonces", JkCommand::new().help("Gets the nonces"))
|
||||||
Arg::new("pairing_file")
|
.with_subcommand(
|
||||||
.long("pairing-file")
|
"app_parameters",
|
||||||
.value_name("PATH")
|
JkCommand::new().help("Gets the app parameters"),
|
||||||
.help("Path to the pairing file"),
|
|
||||||
)
|
)
|
||||||
.arg(
|
.with_subcommand(
|
||||||
Arg::new("udid")
|
"restore_lang",
|
||||||
.value_name("UDID")
|
JkCommand::new()
|
||||||
.help("UDID of the device (overrides host/pairing file)"),
|
.help("Restores the language")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.required(true)
|
||||||
|
.with_help("Language to restore"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.subcommand_required(true)
|
||||||
Arg::new("about")
|
}
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.subcommand(Command::new("delay").about("Delay recovery image"))
|
|
||||||
.subcommand(Command::new("recovery").about("Enter recovery mode"))
|
|
||||||
.subcommand(Command::new("reboot").about("Reboots the device"))
|
|
||||||
.subcommand(Command::new("preflightinfo").about("Gets the preflight info"))
|
|
||||||
.subcommand(Command::new("nonces").about("Gets the nonces"))
|
|
||||||
.subcommand(Command::new("app_parameters").about("Gets the app parameters"))
|
|
||||||
.subcommand(
|
|
||||||
Command::new("restore_lang")
|
|
||||||
.about("Restores the language")
|
|
||||||
.arg(Arg::new("language").required(true).index(1)),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!(
|
|
||||||
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
|
|
||||||
);
|
|
||||||
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, "restore_service-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
let proxy = CoreDeviceProxy::connect(&*provider)
|
let proxy = CoreDeviceProxy::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("no core proxy");
|
.expect("no core proxy");
|
||||||
@@ -88,37 +53,46 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Unable to connect to service");
|
.expect("Unable to connect to service");
|
||||||
|
|
||||||
if matches.subcommand_matches("recovery").is_some() {
|
let (sub_name, sub_args) = arguments.first_subcommand().unwrap();
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"recovery" => {
|
||||||
restore_client
|
restore_client
|
||||||
.enter_recovery()
|
.enter_recovery()
|
||||||
.await
|
.await
|
||||||
.expect("command failed");
|
.expect("command failed");
|
||||||
} else if matches.subcommand_matches("reboot").is_some() {
|
}
|
||||||
|
"reboot" => {
|
||||||
restore_client.reboot().await.expect("command failed");
|
restore_client.reboot().await.expect("command failed");
|
||||||
} else if matches.subcommand_matches("preflightinfo").is_some() {
|
}
|
||||||
|
"preflightinfo" => {
|
||||||
let info = restore_client
|
let info = restore_client
|
||||||
.get_preflightinfo()
|
.get_preflightinfo()
|
||||||
.await
|
.await
|
||||||
.expect("command failed");
|
.expect("command failed");
|
||||||
pretty_print_dictionary(&info);
|
println!("{}", pretty_print_dictionary(&info));
|
||||||
} else if matches.subcommand_matches("nonces").is_some() {
|
}
|
||||||
|
"nonces" => {
|
||||||
let nonces = restore_client.get_nonces().await.expect("command failed");
|
let nonces = restore_client.get_nonces().await.expect("command failed");
|
||||||
pretty_print_dictionary(&nonces);
|
println!("{}", pretty_print_dictionary(&nonces));
|
||||||
} else if matches.subcommand_matches("app_parameters").is_some() {
|
}
|
||||||
|
"app_parameters" => {
|
||||||
let params = restore_client
|
let params = restore_client
|
||||||
.get_app_parameters()
|
.get_app_parameters()
|
||||||
.await
|
.await
|
||||||
.expect("command failed");
|
.expect("command failed");
|
||||||
pretty_print_dictionary(¶ms);
|
println!("{}", pretty_print_dictionary(¶ms));
|
||||||
} else if let Some(matches) = matches.subcommand_matches("restore_lang") {
|
}
|
||||||
let lang = matches
|
"restore_lang" => {
|
||||||
.get_one::<String>("language")
|
let lang: String = sub_args
|
||||||
|
.next_argument::<String>()
|
||||||
.expect("No language passed");
|
.expect("No language passed");
|
||||||
restore_client
|
restore_client
|
||||||
.restore_lang(lang)
|
.restore_lang(lang)
|
||||||
.await
|
.await
|
||||||
.expect("failed to restore lang");
|
.expect("failed to restore lang");
|
||||||
} else {
|
}
|
||||||
eprintln!("Invalid usage, pass -h for help");
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,20 @@
|
|||||||
use clap::{Arg, Command};
|
use idevice::{
|
||||||
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
|
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, provider::IdeviceProvider,
|
||||||
|
rsd::RsdHandshake,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use idevice::screenshotr::ScreenshotService;
|
use idevice::screenshotr::ScreenshotService;
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Take a screenshot")
|
||||||
|
.with_argument(JkArgument::new().with_help("Output path").required(true))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
let output_path = arguments.clone().next_argument::<String>().unwrap();
|
||||||
tracing_subscriber::fmt::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 res = if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
|
||||||
println!("Using DVT over CoreDeviceProxy");
|
println!("Using DVT over CoreDeviceProxy");
|
||||||
@@ -104,7 +55,7 @@ async fn main() {
|
|||||||
screenshot_client.take_screenshot().await.unwrap()
|
screenshot_client.take_screenshot().await.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
match fs::write(output_path, res) {
|
match fs::write(&output_path, res) {
|
||||||
Ok(_) => println!("Screenshot saved to: {}", output_path),
|
Ok(_) => println!("Screenshot saved to: {}", output_path),
|
||||||
Err(e) => eprintln!("Failed to write screenshot to file: {}", e),
|
Err(e) => eprintln!("Failed to write screenshot to file: {}", e),
|
||||||
}
|
}
|
||||||
|
|||||||
161
tools/src/springboardservices.rs
Normal file
161
tools/src/springboardservices.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
|
use plist_macro::{plist_value_to_xml_bytes, pretty_print_plist};
|
||||||
|
|
||||||
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Manage the springboard service")
|
||||||
|
.with_subcommand(
|
||||||
|
"get_icon_state",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Gets the icon state from the device")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Version to query by")
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("save")
|
||||||
|
.with_help("Path to save to")
|
||||||
|
.with_argument(JkArgument::new().required(true)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"set_icon_state",
|
||||||
|
JkCommand::new().help("Sets the icon state").with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("plist to set based on")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"get_wallpaper_preview",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Gets wallpaper preview")
|
||||||
|
.with_subcommand("homescreen", JkCommand::new())
|
||||||
|
.with_subcommand("lockscreen", JkCommand::new())
|
||||||
|
.subcommand_required(true)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("save")
|
||||||
|
.with_help("Path to save the wallpaper PNG file, or preview.png by default")
|
||||||
|
.with_argument(JkArgument::new().required(true)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"get_interface_orientation",
|
||||||
|
JkCommand::new().help("Gets the device's current screen orientation"),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"get_homescreen_icon_metrics",
|
||||||
|
JkCommand::new().help("Gets home screen icon layout metrics"),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"get_icon",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Gets an app's icon as PNG")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Bundle identifier (e.g. com.apple.Maps)")
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("save")
|
||||||
|
.with_help("Path to save the icon PNG file, or icon.png by default")
|
||||||
|
.with_argument(JkArgument::new().required(true)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
|
let mut sbc = SpringBoardServicesClient::connect(&*provider)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to springboardservices");
|
||||||
|
|
||||||
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"get_icon_state" => {
|
||||||
|
let version: Option<String> = sub_args.next_argument();
|
||||||
|
let version = version.as_deref();
|
||||||
|
let state = sbc
|
||||||
|
.get_icon_state(version)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get icon state");
|
||||||
|
println!("{}", pretty_print_plist(&state));
|
||||||
|
|
||||||
|
if let Some(path) = sub_args.get_flag::<String>("save") {
|
||||||
|
tokio::fs::write(path, plist_value_to_xml_bytes(&state))
|
||||||
|
.await
|
||||||
|
.expect("Failed to save to path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"set_icon_state" => {
|
||||||
|
let load_path = sub_args.next_argument::<String>().unwrap();
|
||||||
|
let load = tokio::fs::read(load_path)
|
||||||
|
.await
|
||||||
|
.expect("Failed to read plist");
|
||||||
|
let load: plist::Value =
|
||||||
|
plist::from_bytes(&load).expect("Failed to parse bytes as plist");
|
||||||
|
|
||||||
|
sbc.set_icon_state(load)
|
||||||
|
.await
|
||||||
|
.expect("Failed to set icon state");
|
||||||
|
}
|
||||||
|
"get_wallpaper_preview" => {
|
||||||
|
let (wallpaper_type, _) = sub_args.first_subcommand().unwrap();
|
||||||
|
|
||||||
|
let wallpaper = match wallpaper_type.as_str() {
|
||||||
|
"homescreen" => sbc.get_home_screen_wallpaper_preview_pngdata().await,
|
||||||
|
"lockscreen" => sbc.get_lock_screen_wallpaper_preview_pngdata().await,
|
||||||
|
_ => panic!("Invalid wallpaper type. Use 'homescreen' or 'lockscreen'"),
|
||||||
|
}
|
||||||
|
.expect("Failed to get wallpaper preview");
|
||||||
|
|
||||||
|
let save_path = sub_args
|
||||||
|
.get_flag::<String>("save")
|
||||||
|
.unwrap_or("preview.png".to_string());
|
||||||
|
|
||||||
|
tokio::fs::write(&save_path, wallpaper)
|
||||||
|
.await
|
||||||
|
.expect("Failed to save wallpaper");
|
||||||
|
}
|
||||||
|
"get_interface_orientation" => {
|
||||||
|
let orientation = sbc
|
||||||
|
.get_interface_orientation()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get interface orientation");
|
||||||
|
println!("{:?}", orientation);
|
||||||
|
}
|
||||||
|
"get_homescreen_icon_metrics" => {
|
||||||
|
let metrics = sbc
|
||||||
|
.get_homescreen_icon_metrics()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get homescreen icon metrics");
|
||||||
|
let metrics_value = plist::Value::Dictionary(metrics);
|
||||||
|
println!("{}", pretty_print_plist(&metrics_value));
|
||||||
|
}
|
||||||
|
"get_icon" => {
|
||||||
|
let bundle_id = sub_args.next_argument::<String>().unwrap();
|
||||||
|
|
||||||
|
let icon_data = sbc
|
||||||
|
.get_icon_pngdata(bundle_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get icon");
|
||||||
|
|
||||||
|
let save_path = sub_args
|
||||||
|
.get_flag::<String>("save")
|
||||||
|
.unwrap_or("icon.png".to_string());
|
||||||
|
|
||||||
|
tokio::fs::write(&save_path, icon_data)
|
||||||
|
.await
|
||||||
|
.expect("Failed to save icon");
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +1,13 @@
|
|||||||
// Jackson Coxson
|
// Jackson Coxson
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use idevice::{IdeviceService, provider::IdeviceProvider, syslog_relay::SyslogRelayClient};
|
||||||
use idevice::{IdeviceService, syslog_relay::SyslogRelayClient};
|
use jkcli::{CollectedArguments, JkCommand};
|
||||||
|
|
||||||
mod common;
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new().help("Relay system logs")
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
pub async fn main(_arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
async fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let matches = Command::new("syslog_relay")
|
|
||||||
.about("Relay system logs")
|
|
||||||
.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)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("about")
|
|
||||||
.long("about")
|
|
||||||
.help("Show about information")
|
|
||||||
.action(clap::ArgAction::SetTrue),
|
|
||||||
)
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
if matches.get_flag("about") {
|
|
||||||
println!("Relay logs on 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, "misagent-jkcoxson").await {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut log_client = SyslogRelayClient::connect(&*provider)
|
let mut log_client = SyslogRelayClient::connect(&*provider)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to connect to misagent");
|
.expect("Unable to connect to misagent");
|
||||||
|
|||||||
Reference in New Issue
Block a user