mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Document modules and public methods
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use idevice::{IdeviceError, IdeviceService, lockdown::LockdowndClient};
|
||||
use idevice::{IdeviceError, IdeviceService, lockdown::LockdownClient};
|
||||
|
||||
use crate::{
|
||||
IdeviceErrorCode, IdeviceHandle, IdevicePairingFile, RUNTIME,
|
||||
provider::{TcpProviderHandle, UsbmuxdProviderHandle},
|
||||
};
|
||||
|
||||
pub struct LockdowndClientHandle(pub LockdowndClient);
|
||||
pub struct LockdowndClientHandle(pub LockdownClient);
|
||||
|
||||
/// Connects to lockdownd service using TCP provider
|
||||
///
|
||||
@@ -33,10 +33,10 @@ pub unsafe extern "C" fn lockdownd_connect_tcp(
|
||||
return IdeviceErrorCode::InvalidArg;
|
||||
}
|
||||
|
||||
let res: Result<LockdowndClient, IdeviceError> = RUNTIME.block_on(async move {
|
||||
let res: Result<LockdownClient, IdeviceError> = RUNTIME.block_on(async move {
|
||||
let provider_box = unsafe { Box::from_raw(provider) };
|
||||
let provider_ref = &provider_box.0;
|
||||
let result = LockdowndClient::connect(provider_ref).await;
|
||||
let result = LockdownClient::connect(provider_ref).await;
|
||||
std::mem::forget(provider_box);
|
||||
result
|
||||
});
|
||||
@@ -76,10 +76,10 @@ pub unsafe extern "C" fn lockdownd_connect_usbmuxd(
|
||||
return IdeviceErrorCode::InvalidArg;
|
||||
}
|
||||
|
||||
let res: Result<LockdowndClient, IdeviceError> = RUNTIME.block_on(async move {
|
||||
let res: Result<LockdownClient, IdeviceError> = RUNTIME.block_on(async move {
|
||||
let provider_box = unsafe { Box::from_raw(provider) };
|
||||
let provider_ref = &provider_box.0;
|
||||
let result = LockdowndClient::connect(provider_ref).await;
|
||||
let result = LockdownClient::connect(provider_ref).await;
|
||||
std::mem::forget(provider_box);
|
||||
result
|
||||
});
|
||||
@@ -115,7 +115,7 @@ pub unsafe extern "C" fn lockdownd_new(
|
||||
return IdeviceErrorCode::InvalidArg;
|
||||
}
|
||||
let socket = unsafe { Box::from_raw(socket) }.0;
|
||||
let r = LockdowndClient::new(socket);
|
||||
let r = LockdownClient::new(socket);
|
||||
let boxed = Box::new(LockdowndClientHandle(r));
|
||||
unsafe { *client = Box::into_raw(boxed) };
|
||||
IdeviceErrorCode::IdeviceSuccess
|
||||
|
||||
@@ -7,7 +7,7 @@ use log::warn;
|
||||
use opcode::AfcOpcode;
|
||||
use packet::{AfcPacket, AfcPacketHeader};
|
||||
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
pub mod errors;
|
||||
pub mod opcode;
|
||||
@@ -47,7 +47,7 @@ impl IdeviceService for AfcClient {
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
// Jackson Coxson
|
||||
//! CoreDeviceProxy and CDTunnelPacket utilities for interacting with
|
||||
//! iOS's CoreDeviceProxy service. This service starts an L3 (TUN) tunnel
|
||||
//! for "trusted" services introduced in iOS 17.
|
||||
//!
|
||||
//! This module handles the construction and parsing of `CDTunnelPacket` messages
|
||||
//! and manages a handshake and data tunnel to the CoreDeviceProxy daemon on iOS devices.
|
||||
//!
|
||||
//! # Overview
|
||||
//! - `CDTunnelPacket` is used to parse and serialize packets sent over the CoreDeviceProxy channel.
|
||||
//! - `CoreDeviceProxy` is a service client that initializes the tunnel, handles handshakes,
|
||||
//! and optionally supports creating a software-based TCP/IP tunnel (behind a feature flag).
|
||||
//!
|
||||
//! # Features
|
||||
//! - `tunnel_tcp_stack`: Enables software TCP/IP tunnel creation using a virtual adapter. See the tcp moduel.
|
||||
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// A representation of a CDTunnel packet used in the CoreDeviceProxy protocol.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct CDTunnelPacket {
|
||||
/// The body of the packet, typically JSON-encoded data.
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
@@ -15,27 +30,32 @@ impl CDTunnelPacket {
|
||||
const MAGIC: &'static [u8] = b"CDTunnel";
|
||||
|
||||
/// Parses a byte slice into a `CDTunnelPacket`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - A byte slice containing the raw packet data.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(CDTunnelPacket)` if the input is a valid packet.
|
||||
/// * `Err(IdeviceError)` if parsing fails due to invalid magic, length, or size.
|
||||
pub fn parse(input: &[u8]) -> Result<Self, IdeviceError> {
|
||||
if input.len() < Self::MAGIC.len() + 2 {
|
||||
return Err(IdeviceError::CdtunnelPacketTooShort);
|
||||
}
|
||||
|
||||
// Validate the magic bytes
|
||||
if &input[0..Self::MAGIC.len()] != Self::MAGIC {
|
||||
return Err(IdeviceError::CdtunnelPacketInvalidMagic);
|
||||
}
|
||||
|
||||
// Parse the body length
|
||||
let length_offset = Self::MAGIC.len();
|
||||
let body_length =
|
||||
u16::from_be_bytes([input[length_offset], input[length_offset + 1]]) as usize;
|
||||
|
||||
// Validate the body length
|
||||
if input.len() < length_offset + 2 + body_length {
|
||||
return Err(IdeviceError::PacketSizeMismatch);
|
||||
}
|
||||
|
||||
// Extract the body
|
||||
let body_start = length_offset + 2;
|
||||
let body = input[body_start..body_start + body_length].to_vec();
|
||||
|
||||
@@ -43,37 +63,55 @@ impl CDTunnelPacket {
|
||||
}
|
||||
|
||||
/// Serializes the `CDTunnelPacket` into a byte vector.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the serialized packet.
|
||||
/// * `Err(io::Error)` if writing to the output buffer fails.
|
||||
pub fn serialize(&self) -> io::Result<Vec<u8>> {
|
||||
let mut output = Vec::new();
|
||||
|
||||
// Write the magic bytes
|
||||
output.write_all(Self::MAGIC)?;
|
||||
|
||||
// Write the body length
|
||||
output.write_u16::<BigEndian>(self.body.len() as u16)?;
|
||||
|
||||
// Write the body
|
||||
output.write_all(&self.body)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// A high-level client for the `com.apple.internal.devicecompute.CoreDeviceProxy` service.
|
||||
///
|
||||
/// Handles session negotiation, handshake, and tunnel communication.
|
||||
pub struct CoreDeviceProxy {
|
||||
/// The underlying idevice connection used for communication.
|
||||
pub idevice: Idevice,
|
||||
/// The handshake response received during initialization.
|
||||
pub handshake: HandshakeResponse,
|
||||
/// The maximum transmission unit used for reading.
|
||||
pub mtu: u32,
|
||||
}
|
||||
|
||||
impl IdeviceService for CoreDeviceProxy {
|
||||
/// Returns the name of the service used for launching the CoreDeviceProxy.
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.internal.devicecompute.CoreDeviceProxy"
|
||||
}
|
||||
|
||||
/// Connects to the CoreDeviceProxy service
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `provider` - An implementation of `IdeviceProvider` that supplies
|
||||
/// pairing data and socket connections.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(CoreDeviceProxy)` if connection and handshake succeed.
|
||||
/// * `Err(IdeviceError)` if any step fails.
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
@@ -91,6 +129,7 @@ impl IdeviceService for CoreDeviceProxy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Request sent to initiate the handshake with the CoreDeviceProxy.
|
||||
#[derive(Serialize)]
|
||||
struct HandshakeRequest {
|
||||
#[serde(rename = "type")]
|
||||
@@ -98,13 +137,18 @@ struct HandshakeRequest {
|
||||
mtu: u32,
|
||||
}
|
||||
|
||||
/// Parameters returned as part of the handshake response from the proxy server.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ClientParameters {
|
||||
/// The MTU (maximum transmission unit) for the connection.
|
||||
pub mtu: u16,
|
||||
/// The IP address assigned to the client.
|
||||
pub address: String,
|
||||
/// The subnet mask for the tunnel.
|
||||
pub netmask: String,
|
||||
}
|
||||
|
||||
/// Handshake response structure received from the CoreDeviceProxy.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct HandshakeResponse {
|
||||
#[serde(rename = "clientParameters")]
|
||||
@@ -120,6 +164,16 @@ pub struct HandshakeResponse {
|
||||
impl CoreDeviceProxy {
|
||||
const DEFAULT_MTU: u32 = 16000;
|
||||
|
||||
/// Constructs a new `CoreDeviceProxy` by performing a handshake on the given `Idevice`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `idevice` - The connected `Idevice` socket.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(CoreDeviceProxy)` on successful handshake.
|
||||
/// * `Err(IdeviceError)` if the handshake fails.
|
||||
pub async fn new(mut idevice: Idevice) -> Result<Self, IdeviceError> {
|
||||
let req = HandshakeRequest {
|
||||
packet_type: "clientHandshakeRequest".to_string(),
|
||||
@@ -152,15 +206,37 @@ impl CoreDeviceProxy {
|
||||
})
|
||||
}
|
||||
|
||||
/// Sends a raw data packet through the tunnel.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `data` - The raw bytes to send.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` if the data is successfully sent.
|
||||
/// * `Err(IdeviceError)` if sending fails.
|
||||
pub async fn send(&mut self, data: &[u8]) -> Result<(), IdeviceError> {
|
||||
self.idevice.send_raw(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receives up to `mtu` bytes from the tunnel.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the received data.
|
||||
/// * `Err(IdeviceError)` if reading fails.
|
||||
pub async fn recv(&mut self) -> Result<Vec<u8>, IdeviceError> {
|
||||
self.idevice.read_any(self.mtu).await
|
||||
}
|
||||
|
||||
/// Creates a software-based TCP tunnel adapter, if the `tunnel_tcp_stack` feature is enabled.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Adapter)` for the software TCP stack.
|
||||
/// * `Err(IdeviceError)` if IP parsing or socket extraction fails.
|
||||
#[cfg(feature = "tunnel_tcp_stack")]
|
||||
pub fn create_software_tunnel(self) -> Result<crate::tcp::adapter::Adapter, IdeviceError> {
|
||||
let our_ip = self
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Jackson Coxson
|
||||
// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets
|
||||
//! GDB Remote Debugging Protocol Implementation for iOS Devices
|
||||
//!
|
||||
//! Provides functionality for communicating with the iOS debug server using the
|
||||
//! GDB Remote Serial Protocol as documented at:
|
||||
//! https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets
|
||||
|
||||
use log::debug;
|
||||
use std::fmt::Write;
|
||||
@@ -7,25 +10,47 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{IdeviceError, ReadWrite};
|
||||
|
||||
/// The service name for the debug proxy as registered with lockdownd
|
||||
pub const SERVICE_NAME: &str = "com.apple.internal.dt.remote.debugproxy";
|
||||
|
||||
/// Client for interacting with the iOS debug proxy service
|
||||
///
|
||||
/// Implements the GDB Remote Serial Protocol for communicating with debugserver
|
||||
/// on iOS devices. Handles packet formatting, checksums, and acknowledgments.
|
||||
pub struct DebugProxyClient<R: ReadWrite> {
|
||||
/// The underlying socket connection to debugproxy
|
||||
pub socket: R,
|
||||
/// Flag indicating whether ACK mode is disabled
|
||||
pub noack_mode: bool,
|
||||
}
|
||||
|
||||
/// Represents a debugserver command with arguments
|
||||
///
|
||||
/// Commands follow the GDB Remote Serial Protocol format:
|
||||
/// $<command>[<hex-encoded args>]#<checksum>
|
||||
pub struct DebugserverCommand {
|
||||
/// The command name (e.g. "qSupported", "vCont")
|
||||
pub name: String,
|
||||
/// Command arguments that will be hex-encoded
|
||||
pub argv: Vec<String>,
|
||||
}
|
||||
|
||||
impl DebugserverCommand {
|
||||
/// Creates a new debugserver command
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - The command name (without leading $)
|
||||
/// * `argv` - Arguments that will be hex-encoded in the packet
|
||||
pub fn new(name: String, argv: Vec<String>) -> Self {
|
||||
Self { name, argv }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: ReadWrite> DebugProxyClient<R> {
|
||||
/// Creates a new debug proxy client with default settings
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `socket` - Established connection to debugproxy service
|
||||
pub fn new(socket: R) -> Self {
|
||||
Self {
|
||||
socket,
|
||||
@@ -33,10 +58,24 @@ impl<R: ReadWrite> DebugProxyClient<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the client and returns the underlying socket
|
||||
pub fn into_inner(self) -> R {
|
||||
self.socket
|
||||
}
|
||||
|
||||
/// Sends a command to debugserver and waits for response
|
||||
///
|
||||
/// Formats the command according to GDB Remote Serial Protocol:
|
||||
/// $<command>[<hex-encoded args>]#<checksum>
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `command` - The command and arguments to send
|
||||
///
|
||||
/// # Returns
|
||||
/// The response string if successful, None if no response received
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if communication fails
|
||||
pub async fn send_command(
|
||||
&mut self,
|
||||
command: DebugserverCommand,
|
||||
@@ -69,6 +108,16 @@ impl<R: ReadWrite> DebugProxyClient<R> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Reads a response packet from debugserver
|
||||
///
|
||||
/// Handles the GDB Remote Serial Protocol response format:
|
||||
/// $<data>#<checksum>
|
||||
///
|
||||
/// # Returns
|
||||
/// The response data without protocol framing if successful
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if communication fails or protocol is violated
|
||||
pub async fn read_response(&mut self) -> Result<Option<String>, IdeviceError> {
|
||||
let mut buffer = Vec::new();
|
||||
let mut received_char = [0u8; 1];
|
||||
@@ -103,11 +152,28 @@ impl<R: ReadWrite> DebugProxyClient<R> {
|
||||
Ok(Some(response))
|
||||
}
|
||||
|
||||
/// Sends raw bytes directly to the debugproxy connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bytes` - The raw bytes to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if writing fails
|
||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> {
|
||||
self.socket.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads raw bytes from the debugproxy connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `len` - Maximum number of bytes to read
|
||||
///
|
||||
/// # Returns
|
||||
/// The received data as a string
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if reading fails or data isn't valid UTF-8
|
||||
pub async fn read(&mut self, len: usize) -> Result<String, IdeviceError> {
|
||||
let mut buf = vec![0; len];
|
||||
let r = self.socket.read(&mut buf).await?;
|
||||
@@ -115,6 +181,19 @@ impl<R: ReadWrite> DebugProxyClient<R> {
|
||||
Ok(String::from_utf8_lossy(&buf[..r]).to_string())
|
||||
}
|
||||
|
||||
/// Sets program arguments using the 'A' command
|
||||
///
|
||||
/// Formats arguments according to GDB protocol:
|
||||
/// A<arglen>,<argnum>,<argdata>
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `argv` - Program arguments to set
|
||||
///
|
||||
/// # Returns
|
||||
/// The debugserver response
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if arguments are empty or communication fails
|
||||
pub async fn set_argv(&mut self, argv: Vec<String>) -> Result<String, IdeviceError> {
|
||||
if argv.is_empty() {
|
||||
return Err(IdeviceError::InvalidArgument);
|
||||
@@ -157,26 +236,45 @@ impl<R: ReadWrite> DebugProxyClient<R> {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Sends an acknowledgment (+)
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if writing fails
|
||||
pub async fn send_ack(&mut self) -> Result<(), IdeviceError> {
|
||||
self.socket.write_all(b"+").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a negative acknowledgment (-)
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if writing fails
|
||||
pub async fn send_noack(&mut self) -> Result<(), IdeviceError> {
|
||||
self.socket.write_all(b"-").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enables or disables ACK mode
|
||||
///
|
||||
/// When disabled, the client won't expect or send acknowledgments
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `enabled` - Whether to enable ACK mode
|
||||
pub fn set_ack_mode(&mut self, enabled: bool) {
|
||||
self.noack_mode = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the checksum for a GDB protocol packet
|
||||
///
|
||||
/// The checksum is computed as the modulo 256 sum of all characters
|
||||
/// between '$' and '#', formatted as two lowercase hex digits.
|
||||
fn calculate_checksum(data: &str) -> String {
|
||||
let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte));
|
||||
format!("{:02x}", checksum)
|
||||
}
|
||||
|
||||
/// Hex-encodes bytes as uppercase string
|
||||
fn hex_encode(bytes: &[u8]) -> String {
|
||||
bytes.iter().fold(String::new(), |mut output, b| {
|
||||
let _ = write!(output, "{b:02X}");
|
||||
@@ -185,16 +283,21 @@ fn hex_encode(bytes: &[u8]) -> String {
|
||||
}
|
||||
|
||||
impl From<String> for DebugserverCommand {
|
||||
/// Converts a string into a debugserver command by splitting on whitespace
|
||||
///
|
||||
/// The first token becomes the command name, remaining tokens become arguments
|
||||
fn from(s: String) -> Self {
|
||||
// Split string into command and arguments
|
||||
let mut split = s.split_whitespace();
|
||||
let command = split.next().unwrap_or("").to_string();
|
||||
let arguments: Vec<String> = split.map(|s| s.to_string()).collect();
|
||||
Self::new(command, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for DebugserverCommand {
|
||||
/// Converts a string slice into a debugserver command
|
||||
fn from(s: &str) -> DebugserverCommand {
|
||||
s.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,72 @@
|
||||
// Jackson Coxson
|
||||
//! Location Simulation service client for iOS instruments protocol.
|
||||
//!
|
||||
//! This module abstracts simulating the device's location over
|
||||
//! the remote server protocol. Note that a connection must be
|
||||
//! maintained to keep location simulated.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust,no_run
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), IdeviceError> {
|
||||
//! // Create base client (implementation specific)
|
||||
//! let mut client = RemoteServerClient::new(your_transport);
|
||||
//!
|
||||
//! // Create process control client
|
||||
//! let mut process_control = ProcessControlClient::new(&mut client).await?;
|
||||
//!
|
||||
//! // Launch an app
|
||||
//! let pid = process_control.launch_app(
|
||||
//! "com.example.app",
|
||||
//! None, // Environment variables
|
||||
//! None, // Arguments
|
||||
//! false, // Start suspended
|
||||
//! true // Kill existing
|
||||
//! ).await?;
|
||||
//! println!("Launched app with PID: {}", pid);
|
||||
//!
|
||||
//! // Disable memory limits
|
||||
//! process_control.disable_memory_limit(pid).await?;
|
||||
//!
|
||||
//! // Kill the app
|
||||
//! process_control.kill_app(pid).await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use plist::Value;
|
||||
|
||||
use crate::{dvt::message::AuxValue, IdeviceError, ReadWrite};
|
||||
|
||||
use super::remote_server::{Channel, RemoteServerClient};
|
||||
use crate::{
|
||||
dvt::{
|
||||
message::AuxValue,
|
||||
remote_server::{Channel, RemoteServerClient},
|
||||
},
|
||||
IdeviceError, ReadWrite,
|
||||
};
|
||||
|
||||
const IDENTIFIER: &str = "com.apple.instruments.server.services.LocationSimulation";
|
||||
|
||||
/// A client for the location simulation service
|
||||
pub struct LocationSimulationClient<'a, R: ReadWrite> {
|
||||
/// The underlying channel used for communication
|
||||
channel: Channel<'a, R>,
|
||||
}
|
||||
|
||||
impl<'a, R: ReadWrite> LocationSimulationClient<'a, R> {
|
||||
/// Opens a new channel on the remote server client for location simulation
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The remote server client to connect with
|
||||
///
|
||||
/// # Returns
|
||||
/// The client on success, IdeviceError on failure
|
||||
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
|
||||
let channel = client.make_channel(IDENTIFIER).await?; // Drop `&mut client` before continuing
|
||||
|
||||
Ok(Self { channel })
|
||||
}
|
||||
|
||||
/// Clears the set GPS location
|
||||
pub async fn clear(&mut self) -> Result<(), IdeviceError> {
|
||||
let method = Value::String("stopLocationSimulation".into());
|
||||
|
||||
@@ -29,6 +77,14 @@ impl<'a, R: ReadWrite> LocationSimulationClient<'a, R> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the GPS location
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `latitude` - The f64 latitude value
|
||||
/// * `longitude` - The f64 longitude value
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an IdeviceError on failure
|
||||
pub async fn set(&mut self, latitude: f64, longitude: f64) -> Result<(), IdeviceError> {
|
||||
let method = Value::String("simulateLocationWithLatitude:longitude:".into());
|
||||
|
||||
|
||||
@@ -1,68 +1,172 @@
|
||||
// Jackson Coxson
|
||||
// Messages contain:
|
||||
// - 32 byte header
|
||||
// - 16 byte payload header
|
||||
// - Optional auxiliary
|
||||
// - 16 byte aux header, useless
|
||||
// - Aux data
|
||||
// - Payload (NSKeyedArchive)
|
||||
//! Instruments protocol message format implementation
|
||||
//!
|
||||
//! This module handles the serialization and deserialization of messages used in
|
||||
//! the iOS instruments protocol. The message format consists of:
|
||||
//! - 32-byte message header
|
||||
//! - 16-byte payload header
|
||||
//! - Optional auxiliary data section
|
||||
//! - Payload data (typically NSKeyedArchive format)
|
||||
//!
|
||||
//! # Message Structure
|
||||
//! ```text
|
||||
//! +---------------------+
|
||||
//! | MessageHeader | 32 bytes
|
||||
//! +---------------------+
|
||||
//! | PayloadHeader | 16 bytes
|
||||
//! +---------------------+
|
||||
//! | AuxHeader | 16 bytes (if aux present)
|
||||
//! | Aux data | variable length
|
||||
//! +---------------------+
|
||||
//! | Payload data | variable length (NSKeyedArchive)
|
||||
//! +---------------------+
|
||||
//! ```
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust,no_run
|
||||
//! use plist::Value;
|
||||
//! use your_crate::IdeviceError;
|
||||
//! use your_crate::dvt::message::{Message, MessageHeader, PayloadHeader, AuxValue};
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() -> Result<(), IdeviceError> {
|
||||
//! // Create a new message
|
||||
//! let header = MessageHeader::new(
|
||||
//! 1, // fragment_id
|
||||
//! 1, // fragment_count
|
||||
//! 123, // identifier
|
||||
//! 0, // conversation_index
|
||||
//! 42, // channel
|
||||
//! true // expects_reply
|
||||
//! );
|
||||
//!
|
||||
//! let message = Message::new(
|
||||
//! header,
|
||||
//! PayloadHeader::method_invocation(),
|
||||
//! Some(AuxValue::from_values(vec![
|
||||
//! AuxValue::String("param".into()),
|
||||
//! AuxValue::U32(123),
|
||||
//! ])),
|
||||
//! Some(Value::String("data".into()))
|
||||
//! );
|
||||
//!
|
||||
//! // Serialize message
|
||||
//! let bytes = message.serialize();
|
||||
//!
|
||||
//! // Deserialize message (from async reader)
|
||||
//! # let mut reader = &bytes[..];
|
||||
//! let deserialized = Message::from_reader(&mut reader).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
|
||||
use plist::Value;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
use crate::IdeviceError;
|
||||
|
||||
/// Message header containing metadata about the message
|
||||
///
|
||||
/// 32-byte structure that appears at the start of every message
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MessageHeader {
|
||||
magic: u32, // 0x795b3d1f
|
||||
header_len: u32, // will always be 32 bytes
|
||||
/// Magic number identifying the protocol (0x1F3D5B79)
|
||||
magic: u32,
|
||||
/// Length of this header (always 32)
|
||||
header_len: u32,
|
||||
/// Fragment identifier for multipart messages
|
||||
fragment_id: u16,
|
||||
/// Total number of fragments
|
||||
fragment_count: u16,
|
||||
length: u32, // Length of of the payload
|
||||
/// Total length of payload (headers + aux + data)
|
||||
length: u32,
|
||||
/// Unique message identifier
|
||||
identifier: u32,
|
||||
/// Conversation tracking index
|
||||
conversation_index: u32,
|
||||
/// Channel number this message belongs to
|
||||
pub channel: u32,
|
||||
/// Whether a reply is expected
|
||||
expects_reply: bool,
|
||||
}
|
||||
|
||||
/// Payload header containing information about the message contents
|
||||
///
|
||||
/// 16-byte structure following the message header
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct PayloadHeader {
|
||||
/// Flags controlling message processing
|
||||
flags: u32,
|
||||
/// Length of auxiliary data section
|
||||
aux_length: u32,
|
||||
/// Total length of payload (aux + data)
|
||||
total_length: u64,
|
||||
}
|
||||
|
||||
/// Header for auxiliary data section
|
||||
///
|
||||
/// 16-byte structure preceding auxiliary data
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct AuxHeader {
|
||||
/// Buffer size hint (often 496)
|
||||
buffer_size: u32,
|
||||
/// Unknown field (typically 0)
|
||||
unknown: u32,
|
||||
/// Actual size of auxiliary data
|
||||
aux_size: u32,
|
||||
/// Unknown field (typically 0)
|
||||
unknown2: u32,
|
||||
}
|
||||
|
||||
/// Auxiliary data container
|
||||
///
|
||||
/// Contains a header and a collection of typed values
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Aux {
|
||||
/// Auxiliary data header
|
||||
pub header: AuxHeader,
|
||||
/// Collection of auxiliary values
|
||||
pub values: Vec<AuxValue>,
|
||||
}
|
||||
|
||||
/// Typed auxiliary value that can be included in messages
|
||||
#[derive(PartialEq)]
|
||||
pub enum AuxValue {
|
||||
String(String), // 0x01
|
||||
Array(Vec<u8>), // 0x02
|
||||
U32(u32), // 0x03
|
||||
I64(i64), // 0x06
|
||||
/// UTF-8 string value (type 0x01)
|
||||
String(String),
|
||||
/// Raw byte array (type 0x02)
|
||||
Array(Vec<u8>),
|
||||
/// 32-bit unsigned integer (type 0x03)
|
||||
U32(u32),
|
||||
/// 64-bit signed integer (type 0x06)
|
||||
I64(i64),
|
||||
}
|
||||
|
||||
/// Complete protocol message
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Message {
|
||||
/// Message metadata header
|
||||
pub message_header: MessageHeader,
|
||||
/// Payload description header
|
||||
pub payload_header: PayloadHeader,
|
||||
/// Optional auxiliary data
|
||||
pub aux: Option<Aux>,
|
||||
/// Optional payload data (typically NSKeyedArchive)
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
impl Aux {
|
||||
/// Parses auxiliary data from bytes
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bytes` - Raw byte slice containing auxiliary data
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Aux)` - Parsed auxiliary data
|
||||
/// * `Err(IdeviceError)` - If parsing fails
|
||||
///
|
||||
/// # Errors
|
||||
/// * `IdeviceError::NotEnoughBytes` if input is too short
|
||||
/// * `IdeviceError::UnknownAuxValueType` for unsupported types
|
||||
/// * `IdeviceError` for other parsing failures
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, IdeviceError> {
|
||||
if bytes.len() < 16 {
|
||||
return Err(IdeviceError::NotEnoughBytes(bytes.len(), 24));
|
||||
@@ -129,8 +233,12 @@ impl Aux {
|
||||
Ok(Self { header, values })
|
||||
}
|
||||
|
||||
// Creates the default struct
|
||||
// Note that the header isn't updated until serialization
|
||||
/// Creates new auxiliary data from values
|
||||
///
|
||||
/// Note: Header fields are populated during serialization
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `values` - Collection of auxiliary values to include
|
||||
pub fn from_values(values: Vec<AuxValue>) -> Self {
|
||||
Self {
|
||||
header: AuxHeader::default(),
|
||||
@@ -138,7 +246,9 @@ impl Aux {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes the values with the correctly sized header
|
||||
/// Serializes auxiliary data to bytes
|
||||
///
|
||||
/// Includes properly formatted header with updated size fields
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut values_payload = Vec::new();
|
||||
for v in self.values.iter() {
|
||||
@@ -180,14 +290,27 @@ impl Aux {
|
||||
}
|
||||
|
||||
impl AuxValue {
|
||||
// Returns an array AuxType
|
||||
/// Creates an auxiliary value containing NSKeyedArchived data
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `v` - Plist value to archive
|
||||
pub fn archived_value(v: impl Into<plist::Value>) -> Self {
|
||||
Self::Array(ns_keyed_archive::encode::encode_to_bytes(v.into()).expect("Failed to encode"))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageHeader {
|
||||
/// Creates a new header. Note that during serialization, the length will be updated
|
||||
/// Creates a new message header
|
||||
///
|
||||
/// Note: Length field is updated during message serialization
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `fragment_id` - Identifier for message fragments
|
||||
/// * `fragment_count` - Total fragments in message
|
||||
/// * `identifier` - Unique message ID
|
||||
/// * `conversation_index` - Conversation tracking number
|
||||
/// * `channel` - Channel number
|
||||
/// * `expects_reply` - Whether response is expected
|
||||
pub fn new(
|
||||
fragment_id: u16,
|
||||
fragment_count: u16,
|
||||
@@ -209,6 +332,7 @@ impl MessageHeader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes header to bytes
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
res.extend_from_slice(&self.magic.to_le_bytes());
|
||||
@@ -226,10 +350,12 @@ impl MessageHeader {
|
||||
}
|
||||
|
||||
impl PayloadHeader {
|
||||
/// Creates a new payload header
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Serializes header to bytes
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
res.extend_from_slice(&self.flags.to_le_bytes());
|
||||
@@ -239,6 +365,7 @@ impl PayloadHeader {
|
||||
res
|
||||
}
|
||||
|
||||
/// Creates header for method invocation messages
|
||||
pub fn method_invocation() -> Self {
|
||||
Self {
|
||||
flags: 2,
|
||||
@@ -246,12 +373,24 @@ impl PayloadHeader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates flags to indicate reply expectation
|
||||
pub fn apply_expects_reply_map(&mut self) {
|
||||
self.flags |= 0x1000
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Reads and parses a message from an async reader
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `reader` - Async reader to read from
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Message)` - Parsed message
|
||||
/// * `Err(IdeviceError)` - If reading/parsing fails
|
||||
///
|
||||
/// # Errors
|
||||
/// * Various IdeviceError variants for IO and parsing failures
|
||||
pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Self, IdeviceError> {
|
||||
let mut buf = [0u8; 32];
|
||||
reader.read_exact(&mut buf).await?;
|
||||
@@ -304,6 +443,13 @@ impl Message {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new message
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message_header` - Message metadata
|
||||
/// * `payload_header` - Payload description
|
||||
/// * `aux` - Optional auxiliary data
|
||||
/// * `data` - Optional payload data
|
||||
pub fn new(
|
||||
message_header: MessageHeader,
|
||||
payload_header: PayloadHeader,
|
||||
@@ -318,6 +464,9 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes message to bytes
|
||||
///
|
||||
/// Updates length fields in headers automatically
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let aux = match &self.aux {
|
||||
Some(a) => a.serialize(),
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
// Jackson Coxson
|
||||
//! Process Control service client for iOS instruments protocol.
|
||||
//!
|
||||
//! This module provides a client for interacting with the process control service
|
||||
//! on iOS devices through the instruments protocol. It allows launching, killing,
|
||||
//! and managing processes on the device.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust,no_run
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), IdeviceError> {
|
||||
//! // Create base client (implementation specific)
|
||||
//! let mut client = RemoteServerClient::new(your_transport);
|
||||
//!
|
||||
//! // Create process control client
|
||||
//! let mut process_control = ProcessControlClient::new(&mut client).await?;
|
||||
//!
|
||||
//! // Launch an app
|
||||
//! let pid = process_control.launch_app(
|
||||
//! "com.example.app",
|
||||
//! None, // Environment variables
|
||||
//! None, // Arguments
|
||||
//! false, // Start suspended
|
||||
//! true // Kill existing
|
||||
//! ).await?;
|
||||
//! println!("Launched app with PID: {}", pid);
|
||||
//!
|
||||
//! // Disable memory limits
|
||||
//! process_control.disable_memory_limit(pid).await?;
|
||||
//!
|
||||
//! // Kill the app
|
||||
//! process_control.kill_app(pid).await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use log::warn;
|
||||
use plist::{Dictionary, Value};
|
||||
@@ -9,17 +43,49 @@ use super::remote_server::{Channel, RemoteServerClient};
|
||||
|
||||
const IDENTIFIER: &str = "com.apple.instruments.server.services.processcontrol";
|
||||
|
||||
/// Client for process control operations on iOS devices
|
||||
///
|
||||
/// Provides methods for launching, killing, and managing processes through the
|
||||
/// instruments protocol. Each instance maintains its own communication channel.
|
||||
pub struct ProcessControlClient<'a, R: ReadWrite> {
|
||||
/// The underlying channel for communication
|
||||
channel: Channel<'a, R>,
|
||||
}
|
||||
|
||||
impl<'a, R: ReadWrite> ProcessControlClient<'a, R> {
|
||||
/// Creates a new ProcessControlClient
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client` - The base RemoteServerClient to use
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(ProcessControlClient)` - Connected client instance
|
||||
/// * `Err(IdeviceError)` - If channel creation fails
|
||||
///
|
||||
/// # Errors
|
||||
/// * Propagates errors from channel creation
|
||||
pub async fn new(client: &'a mut RemoteServerClient<R>) -> Result<Self, IdeviceError> {
|
||||
let channel = client.make_channel(IDENTIFIER).await?; // Drop `&mut client` before continuing
|
||||
|
||||
Ok(Self { channel })
|
||||
}
|
||||
|
||||
/// Launches an application on the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bundle_id` - The bundle identifier of the app to launch
|
||||
/// * `env_vars` - Optional environment variables dictionary
|
||||
/// * `arguments` - Optional launch arguments dictionary
|
||||
/// * `start_suspended` - Whether to start the process suspended
|
||||
/// * `kill_existing` - Whether to kill existing instances of the app
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(u64)` - PID of the launched process
|
||||
/// * `Err(IdeviceError)` - If launch fails
|
||||
///
|
||||
/// # Errors
|
||||
/// * `IdeviceError::UnexpectedResponse` if server response is invalid
|
||||
/// * Other communication or serialization errors
|
||||
pub async fn launch_app(
|
||||
&mut self,
|
||||
bundle_id: impl Into<String>,
|
||||
@@ -76,6 +142,17 @@ impl<'a, R: ReadWrite> ProcessControlClient<'a, R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kills a running process
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pid` - Process ID to kill
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - If kill request was sent successfully
|
||||
/// * `Err(IdeviceError)` - If communication fails
|
||||
///
|
||||
/// # Note
|
||||
/// This method doesn't wait for confirmation that the process was killed.
|
||||
pub async fn kill_app(&mut self, pid: u64) -> Result<(), IdeviceError> {
|
||||
self.channel
|
||||
.call_method(
|
||||
@@ -88,6 +165,19 @@ impl<'a, R: ReadWrite> ProcessControlClient<'a, R> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disables memory limits for a process
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pid` - Process ID to modify
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - If memory limits were disabled
|
||||
/// * `Err(IdeviceError)` - If operation fails
|
||||
///
|
||||
/// # Errors
|
||||
/// * `IdeviceError::DisableMemoryLimitFailed` if device reports failure
|
||||
/// * `IdeviceError::UnexpectedResponse` for invalid responses
|
||||
/// * Other communication errors
|
||||
pub async fn disable_memory_limit(&mut self, pid: u64) -> Result<(), IdeviceError> {
|
||||
self.channel
|
||||
.call_method(
|
||||
|
||||
@@ -1,4 +1,53 @@
|
||||
// Jackson Coxson
|
||||
//! Remote Server Client implementation for iOS instruments protocol.
|
||||
//!
|
||||
//! This module provides a client for communicating with iOS devices through the
|
||||
//! remote server protocol used by instruments. It handles channel management and
|
||||
//! message passing between the host and device.
|
||||
//!
|
||||
//! Remote Server communicates via NSKeyedArchives. These archives are binary plists
|
||||
//! formatted specifically for naive recreation at the target.
|
||||
//! Requests are sent as method calls to objective C objects on the device.
|
||||
//!
|
||||
//! # Overview
|
||||
//! The client manages multiple communication channels and provides methods for:
|
||||
//! - Creating new channels
|
||||
//! - Sending method calls
|
||||
//! - Reading responses
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust,no_run
|
||||
//! use std::sync::Arc;
|
||||
//! use tokio::net::TcpStream;
|
||||
//! use your_crate::{ReadWrite, IdeviceError};
|
||||
//! use your_crate::instruments::RemoteServerClient;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), IdeviceError> {
|
||||
//! // Establish connection to device over the tunnel (see XPC docs)
|
||||
//! let transport = TcpStream::connect("1.2.3.4:1234").await?;
|
||||
//!
|
||||
//! // Create client
|
||||
//! let mut client = RemoteServerClient::new(transport);
|
||||
//!
|
||||
//! // Read the first message
|
||||
//! client.read_message(0).await?;
|
||||
//!
|
||||
//! // Call a method on root channel
|
||||
//! client.call_method(
|
||||
//! 0,
|
||||
//! Some("someMethod"),
|
||||
//! Some(vec![AuxValue::String("param".into())]),
|
||||
//! true
|
||||
//! ).await?;
|
||||
//!
|
||||
//! // Read response
|
||||
//! let response = client.read_message(0).await?;
|
||||
//! println!("Got response: {:?}", response);
|
||||
//!
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
@@ -6,27 +55,46 @@ use log::{debug, warn};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::{
|
||||
dvt::message::{Aux, Message, MessageHeader, PayloadHeader},
|
||||
dvt::message::{Aux, AuxValue, Message, MessageHeader, PayloadHeader},
|
||||
IdeviceError, ReadWrite,
|
||||
};
|
||||
|
||||
use super::message::AuxValue;
|
||||
|
||||
/// Message type identifier for instruments protocol
|
||||
pub const INSTRUMENTS_MESSAGE_TYPE: u32 = 2;
|
||||
|
||||
/// Client for communicating with iOS remote server protocol
|
||||
///
|
||||
/// Manages multiple communication channels and handles message serialization/deserialization.
|
||||
/// Each channel operates independently and maintains its own message queue.
|
||||
pub struct RemoteServerClient<R: ReadWrite> {
|
||||
/// The underlying device connection
|
||||
idevice: R,
|
||||
/// Counter for message identifiers
|
||||
current_message: u32,
|
||||
/// Next available channel number
|
||||
new_channel: u32,
|
||||
/// Map of channel numbers to their message queues
|
||||
channels: HashMap<u32, VecDeque<Message>>,
|
||||
}
|
||||
|
||||
/// Handle to a specific communication channel
|
||||
///
|
||||
/// Provides channel-specific operations for use on the remote server client.
|
||||
pub struct Channel<'a, R: ReadWrite> {
|
||||
/// Reference to parent client
|
||||
client: &'a mut RemoteServerClient<R>,
|
||||
/// Channel number this handle operates on
|
||||
channel: u32,
|
||||
}
|
||||
|
||||
impl<R: ReadWrite> RemoteServerClient<R> {
|
||||
/// Creates a new RemoteServerClient with the given transport
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - The underlying transport implementing ReadWrite
|
||||
///
|
||||
/// # Returns
|
||||
/// A new client instance with root channel (0) initialized
|
||||
pub fn new(idevice: R) -> Self {
|
||||
let mut channels = HashMap::new();
|
||||
channels.insert(0, VecDeque::new());
|
||||
@@ -38,10 +106,12 @@ impl<R: ReadWrite> RemoteServerClient<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the client and returns the underlying transport
|
||||
pub fn into_inner(self) -> R {
|
||||
self.idevice
|
||||
}
|
||||
|
||||
/// Returns a handle to the root channel (channel 0)
|
||||
pub fn root_channel(&mut self) -> Channel<R> {
|
||||
Channel {
|
||||
client: self,
|
||||
@@ -49,6 +119,18 @@ impl<R: ReadWrite> RemoteServerClient<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new channel with the given identifier
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `identifier` - String identifier for the new channel
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Channel)` - Handle to the new channel
|
||||
/// * `Err(IdeviceError)` - If channel creation fails
|
||||
///
|
||||
/// # Errors
|
||||
/// * `IdeviceError::UnexpectedResponse` if server responds with unexpected data
|
||||
/// * Other IO or serialization errors
|
||||
pub async fn make_channel(
|
||||
&mut self,
|
||||
identifier: impl Into<String>,
|
||||
@@ -89,6 +171,20 @@ impl<R: ReadWrite> RemoteServerClient<R> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Calls a method on the specified channel
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `channel` - Channel number to call method on
|
||||
/// * `data` - Optional method data (plist value)
|
||||
/// * `args` - Optional arguments for the method
|
||||
/// * `expect_reply` - Whether to expect a response
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - If method was successfully called
|
||||
/// * `Err(IdeviceError)` - If call failed
|
||||
///
|
||||
/// # Errors
|
||||
/// IO or serialization errors
|
||||
pub async fn call_method(
|
||||
&mut self,
|
||||
channel: u32,
|
||||
@@ -110,6 +206,20 @@ impl<R: ReadWrite> RemoteServerClient<R> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the next message from the specified channel
|
||||
///
|
||||
/// Checks cached messages first, then reads from transport if needed.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `channel` - Channel number to read from
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Message)` - The received message
|
||||
/// * `Err(IdeviceError)` - If read failed
|
||||
///
|
||||
/// # Errors
|
||||
/// * `IdeviceError::UnknownChannel` if channel doesn't exist
|
||||
/// * Other IO or deserialization errors
|
||||
pub async fn read_message(&mut self, channel: u32) -> Result<Message, IdeviceError> {
|
||||
// Determine if we already have a message cached
|
||||
let cache = match self.channels.get_mut(&channel) {
|
||||
@@ -140,10 +250,32 @@ impl<R: ReadWrite> RemoteServerClient<R> {
|
||||
}
|
||||
|
||||
impl<R: ReadWrite> Channel<'_, R> {
|
||||
/// Reads the next message from the remote server on this channel
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Message)` - The received message
|
||||
/// * `Err(IdeviceError)` - If read failed
|
||||
///
|
||||
/// # Errors
|
||||
/// * `IdeviceError::UnknownChannel` if channel doesn't exist
|
||||
/// * Other IO or deserialization errors
|
||||
pub async fn read_message(&mut self) -> Result<Message, IdeviceError> {
|
||||
self.client.read_message(self.channel).await
|
||||
}
|
||||
|
||||
/// Calls a method on the specified channel
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `method` - Optional method data (plist value)
|
||||
/// * `args` - Optional arguments for the method
|
||||
/// * `expect_reply` - Whether to expect a response
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - If method was successfully called
|
||||
/// * `Err(IdeviceError)` - If call failed
|
||||
///
|
||||
/// # Errors
|
||||
/// IO or serialization errors
|
||||
pub async fn call_method(
|
||||
&mut self,
|
||||
method: Option<impl Into<plist::Value>>,
|
||||
|
||||
@@ -1,21 +1,49 @@
|
||||
// Jackson Coxson
|
||||
// Abstractions for the heartbeat service on iOS
|
||||
//! iOS Device Heartbeat Service Abstraction
|
||||
//!
|
||||
//! iOS automatically closes service connections if there is no heartbeat client connected and
|
||||
//! responding.
|
||||
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
/// Client for interacting with the iOS device heartbeat service
|
||||
///
|
||||
/// The heartbeat service provides a keep-alive mechanism and can notify when
|
||||
/// the device enters sleep mode or disconnects.
|
||||
/// Note that a running heartbeat client is required to access other services on the device.
|
||||
/// Implements the standard "Marco-Polo" protocol
|
||||
/// where the host sends "Polo" in response to the device's "Marco".
|
||||
pub struct HeartbeatClient {
|
||||
/// The underlying device connection with established heartbeat service
|
||||
pub idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for HeartbeatClient {
|
||||
/// Returns the heartbeat service name as registered with lockdownd
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.mobile.heartbeat"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the heartbeat service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `HeartbeatClient` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any step of the connection process fails
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to lockdownd service
|
||||
/// 2. Starts a lockdown session
|
||||
/// 3. Requests the heartbeat service port
|
||||
/// 4. Establishes connection to the heartbeat port
|
||||
/// 5. Optionally starts TLS if required by service
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
@@ -34,10 +62,31 @@ impl IdeviceService for HeartbeatClient {
|
||||
}
|
||||
|
||||
impl HeartbeatClient {
|
||||
/// Creates a new heartbeat client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Waits for and processes a "Marco" message from the device
|
||||
///
|
||||
/// This will either:
|
||||
/// - Return the heartbeat interval if received
|
||||
/// - Return a timeout error if no message received in time
|
||||
/// - Return a sleep notification if device is going to sleep
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `interval` - Timeout in seconds to wait for message
|
||||
///
|
||||
/// # Returns
|
||||
/// The heartbeat interval in seconds if successful
|
||||
///
|
||||
/// # Errors
|
||||
/// - `HeartbeatTimeout` if no message received before interval
|
||||
/// - `HeartbeatSleepyTime` if device is going to sleep
|
||||
/// - `UnexpectedResponse` for malformed messages
|
||||
pub async fn get_marco(&mut self, interval: u64) -> Result<u64, IdeviceError> {
|
||||
// Get a plist or wait for the interval
|
||||
let rec = tokio::select! {
|
||||
@@ -67,6 +116,13 @@ impl HeartbeatClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a "Polo" response to the device
|
||||
///
|
||||
/// This acknowledges receipt of a "Marco" message and maintains
|
||||
/// the connection keep-alive.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the message fails to send
|
||||
pub async fn send_polo(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Command".into(), "Polo".into());
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
// Jackson Coxson
|
||||
// Incomplete implementation for installation_proxy
|
||||
//! iOS Installation Proxy Service Client
|
||||
//!
|
||||
//! Provides functionality for interacting with the installation_proxy service on iOS devices,
|
||||
//! which allows querying and managing installed applications.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
/// Client for interacting with the iOS installation proxy service
|
||||
///
|
||||
/// This service provides access to information about installed applications
|
||||
/// and can perform application management operations.
|
||||
pub struct InstallationProxyClient {
|
||||
/// The underlying device connection with established installation_proxy service
|
||||
pub idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for InstallationProxyClient {
|
||||
/// Returns the installation proxy service name as registered with lockdownd
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.mobile.installation_proxy"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the installation proxy service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `InstallationProxyClient` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any step of the connection process fails
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to lockdownd service
|
||||
/// 2. Starts a lockdown session
|
||||
/// 3. Requests the installation proxy service port
|
||||
/// 4. Establishes connection to the service port
|
||||
/// 5. Optionally starts TLS if required by service
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
@@ -35,14 +60,39 @@ impl IdeviceService for InstallationProxyClient {
|
||||
}
|
||||
|
||||
impl InstallationProxyClient {
|
||||
/// Creates a new installation proxy client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Gets installed apps on the device
|
||||
/// Retrieves information about installed applications
|
||||
///
|
||||
/// # Arguments
|
||||
/// `application_type` - The application type to filter by
|
||||
/// `bundle_identifiers` - The identifiers to filter by
|
||||
/// * `application_type` - Optional filter for application type:
|
||||
/// - "System" for system applications
|
||||
/// - "User" for user-installed applications
|
||||
/// - "Any" for all applications (default)
|
||||
/// * `bundle_identifiers` - Optional list of specific bundle IDs to query
|
||||
///
|
||||
/// # Returns
|
||||
/// A HashMap mapping bundle identifiers to application information plist values
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The response is malformed
|
||||
/// - The service returns an error
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let apps = client.get_apps(Some("User".to_string()), None).await?;
|
||||
/// for (bundle_id, info) in apps {
|
||||
/// println!("{}: {:?}", bundle_id, info);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_apps(
|
||||
&mut self,
|
||||
application_type: Option<String>,
|
||||
@@ -55,7 +105,7 @@ impl InstallationProxyClient {
|
||||
.into_iter()
|
||||
.map(plist::Value::String)
|
||||
.collect::<Vec<plist::Value>>();
|
||||
options.insert("BundleIDs".into(), ids.into()).unwrap();
|
||||
options.insert("BundleIDs".into(), ids.into());
|
||||
}
|
||||
options.insert("ApplicationType".into(), application_type.into());
|
||||
|
||||
@@ -75,3 +125,4 @@ impl InstallationProxyClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ pub mod dvt;
|
||||
#[cfg(feature = "heartbeat")]
|
||||
pub mod heartbeat;
|
||||
#[cfg(feature = "xpc")]
|
||||
pub mod http2;
|
||||
mod http2;
|
||||
#[cfg(feature = "installation_proxy")]
|
||||
pub mod installation_proxy;
|
||||
pub mod lockdown;
|
||||
@@ -49,24 +49,58 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
pub use util::{pretty_print_dictionary, pretty_print_plist};
|
||||
|
||||
/// A trait combining all required characteristics for a device communication socket
|
||||
///
|
||||
/// This serves as a convenience trait for any type that can be used as an asynchronous
|
||||
/// read/write socket for device communication. Combines common async I/O traits with
|
||||
/// thread safety and debugging requirements.
|
||||
///
|
||||
/// Tokio's TcpStream and UnixStream implement this trait.
|
||||
pub trait ReadWrite: AsyncRead + AsyncWrite + Unpin + Send + Sync + std::fmt::Debug {}
|
||||
|
||||
// Blanket implementation for any compatible type
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + Send + Sync + std::fmt::Debug> ReadWrite for T {}
|
||||
|
||||
/// Interface for services that can be connected to on an iOS device
|
||||
///
|
||||
/// Implement this trait to define new services that can be accessed through the
|
||||
/// device connection protocol.
|
||||
pub trait IdeviceService: Sized {
|
||||
/// Returns the service name as advertised by the device
|
||||
fn service_name() -> &'static str;
|
||||
|
||||
/// Establishes a connection to this service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - The device provider that can supply connections
|
||||
fn connect(
|
||||
provider: &dyn IdeviceProvider,
|
||||
) -> impl std::future::Future<Output = Result<Self, IdeviceError>> + Send;
|
||||
}
|
||||
|
||||
/// Type alias for boxed device connection sockets
|
||||
///
|
||||
/// Used to enable dynamic dispatch of different connection types while maintaining
|
||||
/// the required ReadWrite characteristics.
|
||||
pub type IdeviceSocket = Box<dyn ReadWrite>;
|
||||
|
||||
/// Main handle for communicating with an iOS device
|
||||
///
|
||||
/// Manages the connection socket and provides methods for common device operations
|
||||
/// and message exchange.
|
||||
pub struct Idevice {
|
||||
socket: Option<Box<dyn ReadWrite>>, // in a box for now to use the ReadWrite trait for further uses
|
||||
/// The underlying connection socket, boxed for dynamic dispatch
|
||||
socket: Option<Box<dyn ReadWrite>>,
|
||||
/// Unique label identifying this connection
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl Idevice {
|
||||
/// Creates a new device connection handle
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `socket` - The established connection socket
|
||||
/// * `label` - Unique identifier for this connection
|
||||
pub fn new(socket: Box<dyn ReadWrite>, label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
socket: Some(socket),
|
||||
@@ -74,6 +108,15 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries the device type
|
||||
///
|
||||
/// Sends a QueryType request and parses the response
|
||||
///
|
||||
/// # Returns
|
||||
/// The device type string on success
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if communication fails or response is invalid
|
||||
pub async fn get_type(&mut self) -> Result<String, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Label".into(), self.label.clone().into());
|
||||
@@ -87,6 +130,12 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs RSD (Remote Service Discovery) check-in procedure
|
||||
///
|
||||
/// Establishes the basic service connection protocol
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if the protocol sequence isn't followed correctly
|
||||
pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Label".into(), self.label.clone().into());
|
||||
@@ -116,7 +165,13 @@ impl Idevice {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a plist to the socket
|
||||
/// Sends a plist-formatted message to the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - The plist value to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if serialization or transmission fails
|
||||
async fn send_plist(&mut self, message: plist::Value) -> Result<(), IdeviceError> {
|
||||
if let Some(socket) = &mut self.socket {
|
||||
debug!("Sending plist: {}", pretty_print_plist(&message));
|
||||
@@ -135,11 +190,30 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends raw bytes to the socket
|
||||
/// Sends raw binary data to the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - The bytes to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if transmission fails
|
||||
async fn send_raw(&mut self, message: &[u8]) -> Result<(), IdeviceError> {
|
||||
self.send_raw_with_progress(message, |_| async {}, ()).await
|
||||
}
|
||||
|
||||
/// Sends raw binary data with progress callbacks
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - The bytes to send
|
||||
/// * `callback` - Progress callback invoked after each chunk
|
||||
/// * `state` - Arbitrary state passed to callback
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// * `Fut` - Future type returned by callback
|
||||
/// * `S` - Type of state passed to callback
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if transmission fails
|
||||
async fn send_raw_with_progress<Fut, S>(
|
||||
&mut self,
|
||||
message: &[u8],
|
||||
@@ -165,7 +239,16 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads raw bytes from the socket
|
||||
/// Reads exactly `len` bytes from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `len` - Exact number of bytes to read
|
||||
///
|
||||
/// # Returns
|
||||
/// The received bytes
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if reading fails or connection is closed prematurely
|
||||
async fn read_raw(&mut self, len: usize) -> Result<Vec<u8>, IdeviceError> {
|
||||
if let Some(socket) = &mut self.socket {
|
||||
let mut buf = vec![0; len];
|
||||
@@ -176,7 +259,16 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads bytes from the socket until it doesn't
|
||||
/// Reads up to `max_size` bytes from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `max_size` - Maximum number of bytes to read
|
||||
///
|
||||
/// # Returns
|
||||
/// The received bytes (may be shorter than max_size)
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if reading fails
|
||||
async fn read_any(&mut self, max_size: u32) -> Result<Vec<u8>, IdeviceError> {
|
||||
if let Some(socket) = &mut self.socket {
|
||||
let mut buf = vec![0; max_size as usize];
|
||||
@@ -187,7 +279,13 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a plist from the socket
|
||||
/// Reads a plist-formatted message from the device
|
||||
///
|
||||
/// # Returns
|
||||
/// The parsed plist dictionary
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if reading, parsing fails, or device reports an error
|
||||
async fn read_plist(&mut self) -> Result<plist::Dictionary, IdeviceError> {
|
||||
if let Some(socket) = &mut self.socket {
|
||||
debug!("Reading response size");
|
||||
@@ -213,7 +311,13 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps current connection in TLS
|
||||
/// Upgrades the connection to TLS using device pairing credentials
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pairing_file` - Contains the device's identity and certificates
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if TLS handshake fails or credentials are invalid
|
||||
pub async fn start_session(
|
||||
&mut self,
|
||||
pairing_file: &pairing_file::PairingFile,
|
||||
@@ -235,6 +339,7 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Comprehensive error type for all device communication failures
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum IdeviceError {
|
||||
@@ -362,6 +467,14 @@ pub enum IdeviceError {
|
||||
}
|
||||
|
||||
impl IdeviceError {
|
||||
/// Converts a device-reported error string to a typed error
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `e` - The error string from device
|
||||
/// * `context` - Full plist context containing additional error details
|
||||
///
|
||||
/// # Returns
|
||||
/// Some(IdeviceError) if the string maps to a known error type, None otherwise
|
||||
fn from_device_error_type(e: &str, context: &plist::Dictionary) -> Option<Self> {
|
||||
match e {
|
||||
"GetProhibited" => Some(Self::GetProhibited),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Jackson Coxson
|
||||
// Abstractions for lockdownd
|
||||
//! iOS Lockdown Service Client
|
||||
//!
|
||||
//! Provides functionality for interacting with the lockdown service on iOS devices,
|
||||
//! which is the primary service for device management and service discovery.
|
||||
|
||||
use log::error;
|
||||
use plist::Value;
|
||||
@@ -7,15 +9,33 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{pairing_file, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
pub struct LockdowndClient {
|
||||
/// Client for interacting with the iOS lockdown service
|
||||
///
|
||||
/// This is the primary service for device management and provides:
|
||||
/// - Access to device information and settings
|
||||
/// - Service discovery and port allocation
|
||||
/// - Session management and security
|
||||
pub struct LockdownClient {
|
||||
/// The underlying device connection with established lockdown service
|
||||
pub idevice: crate::Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for LockdowndClient {
|
||||
impl IdeviceService for LockdownClient {
|
||||
/// Returns the lockdown service name as registered with the device
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.mobile.lockdown"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the lockdown service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `LockdownClient` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if connection fails
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
@@ -24,22 +44,48 @@ impl IdeviceService for LockdowndClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal structure for lockdown protocol requests
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct LockdowndRequest {
|
||||
struct LockdownRequest {
|
||||
label: String,
|
||||
key: Option<String>,
|
||||
request: String,
|
||||
}
|
||||
|
||||
impl LockdowndClient {
|
||||
impl LockdownClient {
|
||||
/// The default TCP port for the lockdown service
|
||||
pub const LOCKDOWND_PORT: u16 = 62078;
|
||||
|
||||
/// Creates a new lockdown client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Retrieves a specific value from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `value` - The name of the value to retrieve (e.g., "DeviceName")
|
||||
///
|
||||
/// # Returns
|
||||
/// The requested value as a plist Value
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The requested value doesn't exist
|
||||
/// - The response is malformed
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let device_name = client.get_value("DeviceName").await?;
|
||||
/// println!("Device name: {:?}", device_name);
|
||||
/// ```
|
||||
pub async fn get_value(&mut self, value: impl Into<String>) -> Result<Value, IdeviceError> {
|
||||
let req = LockdowndRequest {
|
||||
let req = LockdownRequest {
|
||||
label: self.idevice.label.clone(),
|
||||
key: Some(value.into()),
|
||||
request: "GetValue".to_string(),
|
||||
@@ -53,8 +99,25 @@ impl LockdowndClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves all available values from the device
|
||||
///
|
||||
/// # Returns
|
||||
/// A dictionary containing all device values
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The response is malformed
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let all_values = client.get_all_values().await?;
|
||||
/// for (key, value) in all_values {
|
||||
/// println!("{}: {:?}", key, value);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_all_values(&mut self) -> Result<plist::Dictionary, IdeviceError> {
|
||||
let req = LockdowndRequest {
|
||||
let req = LockdownRequest {
|
||||
label: self.idevice.label.clone(),
|
||||
key: None,
|
||||
request: "GetValue".to_string(),
|
||||
@@ -68,7 +131,19 @@ impl LockdowndClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a TLS session with the client
|
||||
/// Starts a secure TLS session with the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pairing_file` - Contains the device's identity and certificates
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` on successful session establishment
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - No connection is established
|
||||
/// - The session request is denied
|
||||
/// - TLS handshake fails
|
||||
pub async fn start_session(
|
||||
&mut self,
|
||||
pairing_file: &pairing_file::PairingFile,
|
||||
@@ -116,11 +191,21 @@ impl LockdowndClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Asks lockdownd to pretty please start a service for us
|
||||
/// Requests to start a service on the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// `identifier` - The identifier for the service you want to start
|
||||
/// * `identifier` - The service identifier (e.g., "com.apple.debugserver")
|
||||
///
|
||||
/// # Returns
|
||||
/// The port number and whether to enable SSL on success, `IdeviceError` on failure
|
||||
/// A tuple containing:
|
||||
/// - The port number where the service is available
|
||||
/// - A boolean indicating whether SSL should be used
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - The service cannot be started
|
||||
/// - The response is malformed
|
||||
/// - The requested service doesn't exist
|
||||
pub async fn start_service(
|
||||
&mut self,
|
||||
identifier: impl Into<String>,
|
||||
@@ -144,7 +229,7 @@ impl LockdowndClient {
|
||||
if let Some(port) = port.as_unsigned() {
|
||||
Ok((port as u16, ssl))
|
||||
} else {
|
||||
error!("Port isn't an unsiged integer!");
|
||||
error!("Port isn't an unsigned integer!");
|
||||
Err(IdeviceError::UnexpectedResponse)
|
||||
}
|
||||
}
|
||||
@@ -156,7 +241,8 @@ impl LockdowndClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Idevice> for LockdowndClient {
|
||||
impl From<Idevice> for LockdownClient {
|
||||
/// Converts an existing device connection into a lockdown client
|
||||
fn from(value: Idevice) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
// Jackson Coxson
|
||||
// Incomplete implementation for installation_proxy
|
||||
//! iOS Mobile Installation Agent (misagent) Client
|
||||
//!
|
||||
//! Provides functionality for interacting with the misagent service on iOS devices,
|
||||
//! which manages provisioning profiles and certificates.
|
||||
|
||||
use log::warn;
|
||||
use plist::Dictionary;
|
||||
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
/// Client for interacting with the iOS misagent service
|
||||
///
|
||||
/// The misagent service handles:
|
||||
/// - Installation of provisioning profiles
|
||||
/// - Removal of provisioning profiles
|
||||
/// - Querying installed profiles
|
||||
pub struct MisagentClient {
|
||||
/// The underlying device connection with established misagent service
|
||||
pub idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for MisagentClient {
|
||||
/// Returns the misagent service name as registered with lockdownd
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.misagent"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the misagent service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `MisagentClient` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any step of the connection process fails
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to lockdownd service
|
||||
/// 2. Starts a lockdown session
|
||||
/// 3. Requests the misagent service port
|
||||
/// 4. Establishes connection to the service port
|
||||
/// 5. Optionally starts TLS if required by service
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
@@ -36,15 +63,37 @@ impl IdeviceService for MisagentClient {
|
||||
}
|
||||
|
||||
impl MisagentClient {
|
||||
/// Creates a new misagent client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Installs a provisioning profile on the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `profile` - The provisioning profile data to install
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` on successful installation
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The profile is invalid
|
||||
/// - Installation is not permitted
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let profile_data = std::fs::read("profile.mobileprovision")?;
|
||||
/// client.install(profile_data).await?;
|
||||
/// ```
|
||||
pub async fn install(&mut self, profile: Vec<u8>) -> Result<(), IdeviceError> {
|
||||
let mut req = Dictionary::new();
|
||||
req.insert("MessageType".into(), "Install".into());
|
||||
req.insert("Profile".into(), plist::Value::Data(profile));
|
||||
|
||||
req.insert("ProfileType".into(), "Provisioning".into());
|
||||
|
||||
self.idevice
|
||||
@@ -73,11 +122,28 @@ impl MisagentClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a provisioning profile from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - The UUID of the profile to remove
|
||||
///
|
||||
/// # Returns
|
||||
/// `Ok(())` on successful removal
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The profile doesn't exist
|
||||
/// - Removal is not permitted
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// client.remove("asdf").await?;
|
||||
/// ```
|
||||
pub async fn remove(&mut self, id: &str) -> Result<(), IdeviceError> {
|
||||
let mut req = Dictionary::new();
|
||||
req.insert("MessageType".into(), "Remove".into());
|
||||
req.insert("ProfileID".into(), id.into());
|
||||
|
||||
req.insert("ProfileType".into(), "Provisioning".into());
|
||||
|
||||
self.idevice
|
||||
@@ -106,10 +172,26 @@ impl MisagentClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves all provisioning profiles from the device
|
||||
///
|
||||
/// # Returns
|
||||
/// A vector containing raw profile data for each installed profile
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The response is malformed
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let profiles = client.copy_all().await?;
|
||||
/// for profile in profiles {
|
||||
/// println!("Profile size: {} bytes", profile.len());
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn copy_all(&mut self) -> Result<Vec<Vec<u8>>, IdeviceError> {
|
||||
let mut req = Dictionary::new();
|
||||
req.insert("MessageType".into(), "CopyAll".into());
|
||||
|
||||
req.insert("ProfileType".into(), "Provisioning".into());
|
||||
|
||||
self.idevice
|
||||
@@ -137,3 +219,4 @@ impl MisagentClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
// Jackson Coxson
|
||||
//! iOS Image Mounter Client
|
||||
//!
|
||||
//! Provides functionality for mounting disk images on iOS devices, including:
|
||||
//! - Developer disk images
|
||||
//! - Personalized images
|
||||
//! - Cryptex images
|
||||
//!
|
||||
//! Handles the complete workflow from uploading images to mounting them with proper signatures.
|
||||
|
||||
use log::debug;
|
||||
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
#[cfg(feature = "tss")]
|
||||
use crate::tss::TSSRequest;
|
||||
|
||||
/// Manages mounted images on the idevice.
|
||||
/// NOTE: A lockdown client must be established and queried after establishing a mounter client, or
|
||||
/// the device will stop responding to requests.
|
||||
/// Client for interacting with the iOS mobile image mounter service
|
||||
///
|
||||
/// Manages mounted images on the device.
|
||||
///
|
||||
/// # Important Note
|
||||
/// A lockdown client must be established and queried after establishing a mounter client,
|
||||
/// or the device will stop responding to requests.
|
||||
pub struct ImageMounter {
|
||||
/// The underlying device connection with established image mounter service
|
||||
idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for ImageMounter {
|
||||
/// Returns the image mounter service name as registered with lockdownd
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.mobile.mobile_image_mounter"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the image mounter service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `ImageMounter` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any step of the connection process fails
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to lockdownd service
|
||||
/// 2. Starts a lockdown session
|
||||
/// 3. Requests the image mounter service port
|
||||
/// 4. Establishes connection to the service port
|
||||
/// 5. Optionally starts TLS if required by service
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
@@ -41,10 +71,21 @@ impl IdeviceService for ImageMounter {
|
||||
}
|
||||
|
||||
impl ImageMounter {
|
||||
/// Creates a new image mounter client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Retrieves a list of currently mounted devices
|
||||
///
|
||||
/// # Returns
|
||||
/// A vector of plist values describing mounted devices
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if communication fails or response is malformed
|
||||
pub async fn copy_devices(&mut self) -> Result<Vec<plist::Value>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Command".into(), "CopyDevices".into());
|
||||
@@ -59,7 +100,16 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up an image and returns the signature
|
||||
/// Looks up an image by type and returns its signature
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_type` - The type of image to lookup (e.g., "Developer")
|
||||
///
|
||||
/// # Returns
|
||||
/// The image signature if found
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError::NotFound` if image doesn't exist
|
||||
pub async fn lookup_image(
|
||||
&mut self,
|
||||
image_type: impl Into<String>,
|
||||
@@ -79,6 +129,15 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads an image to the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_type` - Type of image being uploaded
|
||||
/// * `image` - The image data
|
||||
/// * `signature` - Signature for the image
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if upload fails
|
||||
pub async fn upload_image(
|
||||
&mut self,
|
||||
image_type: impl Into<String>,
|
||||
@@ -89,6 +148,21 @@ impl ImageMounter {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Uploads an image with progress callbacks
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_type` - Type of image being uploaded
|
||||
/// * `image` - The image data
|
||||
/// * `signature` - Signature for the image
|
||||
/// * `callback` - Progress callback
|
||||
/// * `state` - State to pass to callback
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// * `Fut` - Future type returned by callback
|
||||
/// * `S` - Type of state passed to callback
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if upload fails
|
||||
pub async fn upload_image_with_progress<Fut, S>(
|
||||
&mut self,
|
||||
image_type: impl Into<String>,
|
||||
@@ -149,6 +223,16 @@ impl ImageMounter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mounts an image on the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_type` - Type of image to mount
|
||||
/// * `signature` - Signature for the image
|
||||
/// * `trust_cache` - Optional trust cache data
|
||||
/// * `info_plist` - Optional info plist for the image
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if mounting fails
|
||||
pub async fn mount_image(
|
||||
&mut self,
|
||||
image_type: impl Into<String>,
|
||||
@@ -187,9 +271,15 @@ impl ImageMounter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmounts an image at a specified path.
|
||||
/// Use ``/Developer`` for pre-iOS 17 developer images.
|
||||
/// Use ``/System/Developer`` for personalized images.
|
||||
/// Unmounts an image at the specified path
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `mount_path` - Path where image is mounted:
|
||||
/// - `/Developer` for pre-iOS 17 developer images
|
||||
/// - `/System/Developer` for personalized images
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if unmounting fails
|
||||
pub async fn unmount_image(
|
||||
&mut self,
|
||||
mount_path: impl Into<String>,
|
||||
@@ -209,8 +299,20 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries the personalization manifest from the device.
|
||||
/// Queries the personalization manifest from the device
|
||||
///
|
||||
/// # Important
|
||||
/// On failure, the socket must be closed and reestablished.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_type` - Type of image to query manifest for
|
||||
/// * `signature` - Signature of the image
|
||||
///
|
||||
/// # Returns
|
||||
/// The personalization manifest data
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if query fails
|
||||
pub async fn query_personalization_manifest(
|
||||
&mut self,
|
||||
image_type: impl Into<String>,
|
||||
@@ -234,6 +336,13 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries the developer mode status of the device
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` if developer mode is enabled, `false` otherwise
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if query fails
|
||||
pub async fn query_developer_mode_status(&mut self) -> Result<bool, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Command".into(), "QueryDeveloperModeStatus".into());
|
||||
@@ -248,6 +357,16 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries the nonce value from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `personalized_image_type` - Optional image type to get nonce for
|
||||
///
|
||||
/// # Returns
|
||||
/// The nonce value
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if query fails
|
||||
pub async fn query_nonce(
|
||||
&mut self,
|
||||
personalized_image_type: Option<String>,
|
||||
@@ -268,6 +387,16 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries personalization identifiers from the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image_type` - Optional image type to get identifiers for
|
||||
///
|
||||
/// # Returns
|
||||
/// Dictionary of personalization identifiers
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if query fails
|
||||
pub async fn query_personalization_identifiers(
|
||||
&mut self,
|
||||
image_type: Option<String>,
|
||||
@@ -288,6 +417,10 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rolls the personalization nonce on the device
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if operation fails
|
||||
pub async fn roll_personalization_nonce(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Command".into(), "RollPersonalizationNonce".into());
|
||||
@@ -298,6 +431,10 @@ impl ImageMounter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rolls the cryptex nonce on the device
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if operation fails
|
||||
pub async fn roll_cryptex_nonce(&mut self) -> Result<(), IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("Command".into(), "RollCryptexNonce".into());
|
||||
@@ -308,6 +445,14 @@ impl ImageMounter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mounts a developer disk image
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `image` - The developer disk image data
|
||||
/// * `signature` - Signature for the image
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if mounting fails
|
||||
pub async fn mount_developer(
|
||||
&mut self,
|
||||
image: &[u8],
|
||||
@@ -321,6 +466,18 @@ impl ImageMounter {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tss")]
|
||||
/// Mounts a personalized image with automatic manifest handling
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider (used for reconnection if needed)
|
||||
/// * `image` - The image data
|
||||
/// * `trust_cache` - Trust cache data
|
||||
/// * `build_manifest` - Build manifest data
|
||||
/// * `info_plist` - Optional info plist for the image
|
||||
/// * `unique_chip_id` - Device's unique chip ID
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if mounting fails
|
||||
pub async fn mount_personalized(
|
||||
&mut self,
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
@@ -344,9 +501,28 @@ impl ImageMounter {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tss")]
|
||||
/// Calling this has the potential of closing the socket,
|
||||
/// so a provider is required for this abstraction.
|
||||
#[allow(clippy::too_many_arguments)] // literally nobody asked
|
||||
/// Mounts a personalized image with progress callbacks
|
||||
///
|
||||
/// # Important
|
||||
/// This may close the socket on failure, requiring reconnection.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
/// * `image` - The image data
|
||||
/// * `trust_cache` - Trust cache data
|
||||
/// * `build_manifest` - Build manifest data
|
||||
/// * `info_plist` - Optional info plist for the image
|
||||
/// * `unique_chip_id` - Device's unique chip ID
|
||||
/// * `callback` - Progress callback
|
||||
/// * `state` - State to pass to callback
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// * `Fut` - Future type returned by callback
|
||||
/// * `S` - Type of state passed to callback
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if mounting fails
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn mount_personalized_with_callback<Fut, S>(
|
||||
&mut self,
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
@@ -385,7 +561,7 @@ impl ImageMounter {
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Uploading imaage");
|
||||
debug!("Uploading image");
|
||||
self.upload_image_with_progress("Personalized", &image, manifest.clone(), callback, state)
|
||||
.await?;
|
||||
|
||||
@@ -397,6 +573,17 @@ impl ImageMounter {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tss")]
|
||||
/// Retrieves a personalization manifest from Apple's TSS server
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `build_manifest` - Build manifest dictionary
|
||||
/// * `unique_chip_id` - Device's unique chip ID
|
||||
///
|
||||
/// # Returns
|
||||
/// The manifest data
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if manifest retrieval fails
|
||||
pub async fn get_manifest_from_tss(
|
||||
&mut self,
|
||||
build_manifest: &plist::Dictionary,
|
||||
@@ -589,3 +776,4 @@ impl ImageMounter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Jackson Coxson
|
||||
//! iOS Device Pairing File Handling
|
||||
//!
|
||||
//! Provides functionality for reading, writing, and manipulating iOS device pairing files
|
||||
//! which contain the cryptographic materials needed for secure communication with devices.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -7,20 +10,35 @@ use plist::Data;
|
||||
use rustls::pki_types::{pem::PemObject, CertificateDer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a complete iOS device pairing record
|
||||
///
|
||||
/// Contains all cryptographic materials and identifiers needed for secure communication
|
||||
/// with an iOS device, including certificates, private keys, and device identifiers.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PairingFile {
|
||||
/// Device's certificate in DER format
|
||||
pub device_certificate: CertificateDer<'static>,
|
||||
pub host_private_key: Vec<u8>, // the private key doesn't implement clone...
|
||||
/// Host's private key in DER format
|
||||
pub host_private_key: Vec<u8>,
|
||||
/// Host's certificate in DER format
|
||||
pub host_certificate: CertificateDer<'static>,
|
||||
/// Root CA's private key in DER format
|
||||
pub root_private_key: Vec<u8>,
|
||||
/// Root CA's certificate in DER format
|
||||
pub root_certificate: CertificateDer<'static>,
|
||||
/// System Build Unique Identifier
|
||||
pub system_buid: String,
|
||||
/// Host identifier
|
||||
pub host_id: String,
|
||||
/// Escrow bag allowing for access while locked
|
||||
pub escrow_bag: Vec<u8>,
|
||||
/// Device's WiFi MAC address
|
||||
pub wifi_mac_address: String,
|
||||
/// Device's Unique Device Identifier (optional)
|
||||
pub udid: Option<String>,
|
||||
}
|
||||
|
||||
/// Internal representation of a pairing file for serialization/deserialization
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct RawPairingFile {
|
||||
@@ -41,11 +59,37 @@ struct RawPairingFile {
|
||||
}
|
||||
|
||||
impl PairingFile {
|
||||
/// Reads a pairing file from disk
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - Path to the pairing file (typically a .plist file)
|
||||
///
|
||||
/// # Returns
|
||||
/// A parsed `PairingFile` on success
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - The file cannot be read
|
||||
/// - The contents are malformed
|
||||
/// - Cryptographic materials are invalid
|
||||
pub fn read_from_file(path: impl AsRef<Path>) -> Result<Self, crate::IdeviceError> {
|
||||
let f = std::fs::read(path)?;
|
||||
Self::from_bytes(&f)
|
||||
}
|
||||
|
||||
/// Parses a pairing file from raw bytes
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bytes` - Raw bytes of the pairing file (typically PLIST format)
|
||||
///
|
||||
/// # Returns
|
||||
/// A parsed `PairingFile` on success
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - The data cannot be parsed as PLIST
|
||||
/// - Required fields are missing
|
||||
/// - Cryptographic materials are invalid
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::IdeviceError> {
|
||||
let r = match ::plist::from_bytes::<RawPairingFile>(bytes) {
|
||||
Ok(r) => r,
|
||||
@@ -64,12 +108,31 @@ impl PairingFile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a pairing file from a plist value
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `v` - PLIST value containing pairing data
|
||||
///
|
||||
/// # Returns
|
||||
/// A parsed `PairingFile` on success
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Required fields are missing
|
||||
/// - Cryptographic materials are invalid
|
||||
pub fn from_value(v: &plist::Value) -> Result<Self, crate::IdeviceError> {
|
||||
let raw: RawPairingFile = plist::from_value(v)?;
|
||||
let p = raw.try_into()?;
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// Serializes the pairing file to a PLIST-formatted byte vector
|
||||
///
|
||||
/// # Returns
|
||||
/// A byte vector containing the serialized pairing file
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if serialization fails
|
||||
pub fn serialize(self) -> Result<Vec<u8>, crate::IdeviceError> {
|
||||
let raw = RawPairingFile::from(self);
|
||||
|
||||
@@ -82,6 +145,9 @@ impl PairingFile {
|
||||
impl TryFrom<RawPairingFile> for PairingFile {
|
||||
type Error = rustls::pki_types::pem::Error;
|
||||
|
||||
/// Attempts to convert a raw pairing file into a structured pairing file
|
||||
///
|
||||
/// Performs validation of cryptographic materials during conversion.
|
||||
fn try_from(value: RawPairingFile) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
device_certificate: CertificateDer::from_pem_slice(&Into::<Vec<u8>>::into(
|
||||
@@ -105,6 +171,7 @@ impl TryFrom<RawPairingFile> for PairingFile {
|
||||
}
|
||||
|
||||
impl From<PairingFile> for RawPairingFile {
|
||||
/// Converts a structured pairing file into a raw pairing file for serialization
|
||||
fn from(value: PairingFile) -> Self {
|
||||
Self {
|
||||
device_certificate: Data::new(value.device_certificate.to_vec()),
|
||||
@@ -122,7 +189,7 @@ impl From<PairingFile> for RawPairingFile {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f1() {
|
||||
fn test_pairing_file_roundtrip() {
|
||||
let f = std::fs::read("/var/lib/lockdown/test.plist").unwrap();
|
||||
|
||||
println!("{}", String::from_utf8_lossy(&f));
|
||||
@@ -133,3 +200,4 @@ fn f1() {
|
||||
|
||||
assert_eq!(f[..output.len()], output);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Jackson Coxson
|
||||
//! iOS Device Connection Providers
|
||||
//!
|
||||
//! Provides abstractions for establishing connections to iOS devices through different
|
||||
//! transport mechanisms (TCP, USB, etc.).
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
@@ -14,31 +17,56 @@ use crate::{pairing_file::PairingFile, Idevice, IdeviceError};
|
||||
#[cfg(feature = "usbmuxd")]
|
||||
use crate::usbmuxd::UsbmuxdAddr;
|
||||
|
||||
/// A provider for connecting to the iOS device
|
||||
/// This is an ugly trait until async traits are stabilized
|
||||
/// Trait for providers that can establish connections to iOS devices
|
||||
///
|
||||
/// This is an async trait that abstracts over different connection methods
|
||||
/// (TCP, USB, etc.).
|
||||
pub trait IdeviceProvider: Unpin + Send + Sync + std::fmt::Debug {
|
||||
/// Establishes a connection to the specified port on the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `port` - The port number to connect to
|
||||
///
|
||||
/// # Returns
|
||||
/// A future that resolves to an `Idevice` connection handle
|
||||
fn connect(
|
||||
&self,
|
||||
port: u16,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Idevice, IdeviceError>> + Send>>;
|
||||
|
||||
/// Returns a label identifying this provider/connection
|
||||
fn label(&self) -> &str;
|
||||
|
||||
/// Retrieves the pairing file needed for secure communication
|
||||
///
|
||||
/// # Returns
|
||||
/// A future that resolves to the device's `PairingFile`
|
||||
fn get_pairing_file(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = Result<PairingFile, IdeviceError>> + Send>>;
|
||||
}
|
||||
|
||||
/// TCP-based device connection provider
|
||||
#[cfg(feature = "tcp")]
|
||||
#[derive(Debug)]
|
||||
pub struct TcpProvider {
|
||||
/// IP address of the device
|
||||
pub addr: IpAddr,
|
||||
/// Pairing file for secure communication
|
||||
pub pairing_file: PairingFile,
|
||||
/// Label identifying this connection
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
impl IdeviceProvider for TcpProvider {
|
||||
/// Connects to the device over TCP
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `port` - The TCP port to connect to
|
||||
///
|
||||
/// # Returns
|
||||
/// An `Idevice` wrapped in a future
|
||||
fn connect(
|
||||
&self,
|
||||
port: u16,
|
||||
@@ -52,10 +80,12 @@ impl IdeviceProvider for TcpProvider {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the connection label
|
||||
fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
|
||||
/// Returns the pairing file (cloned from the provider)
|
||||
fn get_pairing_file(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = Result<PairingFile, IdeviceError>> + Send>> {
|
||||
@@ -64,18 +94,31 @@ impl IdeviceProvider for TcpProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// USB-based device connection provider using usbmuxd
|
||||
#[cfg(feature = "usbmuxd")]
|
||||
#[derive(Debug)]
|
||||
pub struct UsbmuxdProvider {
|
||||
/// USB connection address
|
||||
pub addr: UsbmuxdAddr,
|
||||
/// Connection tag/identifier
|
||||
pub tag: u32,
|
||||
/// Device UDID
|
||||
pub udid: String,
|
||||
/// Device ID
|
||||
pub device_id: u32,
|
||||
/// Connection label
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "usbmuxd")]
|
||||
impl IdeviceProvider for UsbmuxdProvider {
|
||||
/// Connects to the device over USB via usbmuxd
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `port` - The port number to connect to on the device
|
||||
///
|
||||
/// # Returns
|
||||
/// An `Idevice` wrapped in a future
|
||||
fn connect(
|
||||
&self,
|
||||
port: u16,
|
||||
@@ -91,10 +134,12 @@ impl IdeviceProvider for UsbmuxdProvider {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the connection label
|
||||
fn label(&self) -> &str {
|
||||
&self.label
|
||||
}
|
||||
|
||||
/// Retrieves the pairing record from usbmuxd
|
||||
fn get_pairing_file(
|
||||
&self,
|
||||
) -> Pin<Box<dyn Future<Output = Result<PairingFile, IdeviceError>> + Send>> {
|
||||
@@ -108,3 +153,4 @@ impl IdeviceProvider for UsbmuxdProvider {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
use crate::{lockdown::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
//! SpringBoard Services Client
|
||||
//!
|
||||
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
|
||||
//! which manages home screen and app icon related operations.
|
||||
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
/// Client for interacting with the iOS SpringBoard services
|
||||
///
|
||||
/// This service provides access to home screen and app icon functionality,
|
||||
/// such as retrieving app icons.
|
||||
pub struct SpringBoardServicesClient {
|
||||
/// The underlying device connection with established SpringBoard services
|
||||
pub idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for SpringBoardServicesClient {
|
||||
/// Returns the SpringBoard services name as registered with lockdownd
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.springboardservices"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the SpringBoard services
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `SpringBoardServicesClient` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any step of the connection process fails
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to lockdownd service
|
||||
/// 2. Starts a lockdown session
|
||||
/// 3. Requests the SpringBoard services port
|
||||
/// 4. Establishes connection to the service port
|
||||
/// 5. Optionally starts TLS if required by service
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
@@ -31,13 +59,33 @@ impl IdeviceService for SpringBoardServicesClient {
|
||||
}
|
||||
|
||||
impl SpringBoardServicesClient {
|
||||
/// Creates a new SpringBoard services client from an existing device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `idevice` - Pre-established device connection
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self { idevice }
|
||||
}
|
||||
|
||||
/// Gets the icon of a spceified app
|
||||
/// Retrieves the PNG icon data for a specified app
|
||||
///
|
||||
/// # Arguments
|
||||
/// `bundle_identifier` - The identifier of the app to get icon
|
||||
/// * `bundle_identifier` - The bundle identifier of the app (e.g., "com.apple.Maps")
|
||||
///
|
||||
/// # Returns
|
||||
/// The raw PNG data of the app icon
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - The app doesn't exist
|
||||
/// - The response is malformed
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let icon_data = client.get_icon_pngdata("com.apple.Maps".to_string()).await?;
|
||||
/// std::fs::write("maps_icon.png", icon_data)?;
|
||||
/// ```
|
||||
pub async fn get_icon_pngdata(
|
||||
&mut self,
|
||||
bundle_identifier: String,
|
||||
@@ -56,3 +104,4 @@ impl SpringBoardServicesClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
// Jackson Coxson
|
||||
// Simpler TCP stack
|
||||
//! A simplified TCP network stack implementation.
|
||||
//!
|
||||
//! This module provides a naive TCP stack implementation designed for simple,
|
||||
//! reliable network environments. It handles basic TCP operations while making
|
||||
//! significant simplifying assumptions about the underlying transport.
|
||||
//!
|
||||
//! # Features
|
||||
//! - Basic TCP connection establishment (3-way handshake)
|
||||
//! - Data transmission with PSH flag
|
||||
//! - Connection teardown
|
||||
//! - Optional PCAP packet capture
|
||||
//! - Implements `AsyncRead` and `AsyncWrite` for Tokio compatibility
|
||||
//!
|
||||
//! # Limitations
|
||||
//! - Only supports one connection at a time
|
||||
//! - No proper sequence number tracking
|
||||
//! - No retransmission or congestion control
|
||||
//! - Requires 100% reliable underlying transport
|
||||
//! - Minimal error handling
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust,no_run
|
||||
//! use std::net::{IpAddr, Ipv4Addr};
|
||||
//! use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
//! use your_crate::tcp::Adapter;
|
||||
//! use your_crate::ReadWrite; // Assuming you have a ReadWrite trait
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Create a transport connection (this would be your actual transport)
|
||||
//! let transport = /* your transport implementing ReadWrite */;
|
||||
//!
|
||||
//! // Create TCP adapter
|
||||
//! let host_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2));
|
||||
//! let peer_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
|
||||
//! let mut adapter = Adapter::new(Box::new(transport), host_ip, peer_ip);
|
||||
//!
|
||||
//! // Optional: enable packet capture
|
||||
//! adapter.pcap("capture.pcap").await?;
|
||||
//!
|
||||
//! // Connect to remote server
|
||||
//! adapter.connect(80).await?;
|
||||
//!
|
||||
//! // Send HTTP request
|
||||
//! adapter.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").await?;
|
||||
//! adapter.flush().await?;
|
||||
//!
|
||||
//! // Read response
|
||||
//! let mut buf = vec![0; 1024];
|
||||
//! let n = adapter.read(&mut buf).await?;
|
||||
//! println!("Received: {}", String::from_utf8_lossy(&buf[..n]));
|
||||
//!
|
||||
//! // Close connection
|
||||
//! adapter.close().await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Warning
|
||||
//! This implementation makes significant simplifications and should not be used
|
||||
//! in production environments or with unreliable network transports.
|
||||
|
||||
use std::{future::Future, net::IpAddr, path::Path, sync::Arc, task::Poll};
|
||||
|
||||
@@ -19,32 +79,57 @@ enum AdapterState {
|
||||
None,
|
||||
}
|
||||
|
||||
/// An extremely naive, limited and dangerous stack
|
||||
/// Only one connection can be live at a time
|
||||
/// Acks aren't tracked, and are silently ignored
|
||||
/// Only use when the underlying transport is 100% reliable
|
||||
/// A simplified TCP network stack implementation.
|
||||
///
|
||||
/// This is an extremely naive, limited, and dangerous TCP stack implementation.
|
||||
/// Key limitations:
|
||||
/// - Only one connection can be active at a time
|
||||
/// - ACKs aren't properly tracked and are silently ignored
|
||||
/// - Should only be used when the underlying transport is 100% reliable
|
||||
///
|
||||
/// The adapter implements `AsyncRead` and `AsyncWrite` for convenient IO operations.
|
||||
#[derive(Debug)]
|
||||
pub struct Adapter {
|
||||
/// The underlying transport connection
|
||||
peer: Box<dyn ReadWrite>,
|
||||
/// The local IP address
|
||||
host_ip: IpAddr,
|
||||
/// The remote peer's IP address
|
||||
peer_ip: IpAddr,
|
||||
/// Current connection state
|
||||
state: AdapterState,
|
||||
|
||||
// TCP state
|
||||
/// Current sequence number
|
||||
seq: u32,
|
||||
/// Current acknowledgement number
|
||||
ack: u32,
|
||||
/// Local port number
|
||||
host_port: u16,
|
||||
/// Remote port number
|
||||
peer_port: u16,
|
||||
|
||||
// Read buffer to cache unused bytes
|
||||
/// Buffer for storing unread received data
|
||||
read_buffer: Vec<u8>,
|
||||
/// Buffer for storing data to be sent
|
||||
write_buffer: Vec<u8>,
|
||||
|
||||
// Logging
|
||||
/// Optional PCAP file for packet logging
|
||||
pcap: Option<Arc<Mutex<tokio::fs::File>>>,
|
||||
}
|
||||
|
||||
impl Adapter {
|
||||
/// Creates a new TCP adapter instance.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `peer` - The underlying transport connection implementing `ReadWrite`
|
||||
/// * `host_ip` - The local IP address to use
|
||||
/// * `peer_ip` - The remote IP address to connect to
|
||||
///
|
||||
/// # Returns
|
||||
/// A new unconnected `Adapter` instance
|
||||
pub fn new(peer: Box<dyn ReadWrite>, host_ip: IpAddr, peer_ip: IpAddr) -> Self {
|
||||
Self {
|
||||
peer,
|
||||
@@ -61,6 +146,18 @@ impl Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates a TCP connection to the specified port.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `port` - The remote port number to connect to
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if connection was successful
|
||||
/// * `Err(std::io::Error)` if connection failed
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns `InvalidData` if the SYN-ACK response is invalid
|
||||
/// * Returns other IO errors if underlying transport fails
|
||||
pub async fn connect(&mut self, port: u16) -> Result<(), std::io::Error> {
|
||||
self.read_buffer = Vec::new();
|
||||
self.write_buffer = Vec::new();
|
||||
@@ -110,6 +207,14 @@ impl Adapter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enables packet capture to a PCAP file.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `path` - The filesystem path to write the PCAP data to
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if PCAP file was successfully created
|
||||
/// * `Err(std::io::Error)` if file creation failed
|
||||
pub async fn pcap(&mut self, path: impl AsRef<Path>) -> Result<(), std::io::Error> {
|
||||
let mut file = tokio::fs::File::create(path).await?;
|
||||
|
||||
@@ -134,6 +239,14 @@ impl Adapter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Closes the TCP connection.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if connection was closed cleanly
|
||||
/// * `Err(std::io::Error)` if closing failed
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns IO errors if underlying transport fails during close
|
||||
pub async fn close(&mut self) -> Result<(), std::io::Error> {
|
||||
let tcp_packet = TcpPacket::create(
|
||||
self.host_ip,
|
||||
@@ -191,7 +304,17 @@ impl Adapter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a packet
|
||||
/// Sends a TCP packet with PSH flag set (pushing data).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - The payload data to send
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if data was sent successfully
|
||||
/// * `Err(std::io::Error)` if sending failed
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns IO errors if underlying transport fails
|
||||
pub async fn psh(&mut self, data: &[u8]) -> Result<(), std::io::Error> {
|
||||
trace!("pshing {} bytes", data.len());
|
||||
let tcp_packet = TcpPacket::create(
|
||||
@@ -230,6 +353,15 @@ impl Adapter {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receives data from the connection.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Vec<u8>)` containing received data
|
||||
/// * `Err(std::io::Error)` if receiving failed
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns `ConnectionReset` if connection was reset or closed
|
||||
/// * Returns other IO errors if underlying transport fails
|
||||
pub async fn recv(&mut self) -> Result<Vec<u8>, std::io::Error> {
|
||||
loop {
|
||||
let res = self.read_tcp_packet().await?;
|
||||
@@ -324,6 +456,18 @@ impl Adapter {
|
||||
}
|
||||
|
||||
impl AsyncRead for Adapter {
|
||||
/// Attempts to read from the connection into the provided buffer.
|
||||
///
|
||||
/// Uses an internal read buffer to cache any extra received data.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Poll::Ready(Ok(()))` if data was read successfully
|
||||
/// * `Poll::Ready(Err(e))` if an error occurred
|
||||
/// * `Poll::Pending` if operation would block
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns `NotConnected` if adapter isn't connected
|
||||
/// * Propagates any underlying transport errors
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
@@ -377,6 +521,17 @@ impl AsyncRead for Adapter {
|
||||
}
|
||||
|
||||
impl AsyncWrite for Adapter {
|
||||
/// Attempts to write data to the connection.
|
||||
///
|
||||
/// Data is buffered internally until flushed.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Poll::Ready(Ok(n))` with number of bytes written
|
||||
/// * `Poll::Ready(Err(e))` if an error occurred
|
||||
/// * `Poll::Pending` if operation would block
|
||||
///
|
||||
/// # Errors
|
||||
/// * Returns `NotConnected` if adapter isn't connected
|
||||
fn poll_write(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
// Jackson Coxson
|
||||
// Thanks pymobiledevice3
|
||||
//! Ticket Signature Server (TSS) Client
|
||||
//!
|
||||
//! Provides functionality for interacting with Apple's TSS service to:
|
||||
//! - Request personalized firmware components
|
||||
//! - Apply restore request rules for device-specific parameters
|
||||
//! - Handle cryptographic signing operations
|
||||
|
||||
use log::{debug, warn};
|
||||
use plist::Value;
|
||||
|
||||
use crate::{util::plist_to_xml_bytes, IdeviceError};
|
||||
|
||||
/// TSS client version string sent in requests
|
||||
const TSS_CLIENT_VERSION_STRING: &str = "libauthinstall-1033.0.2";
|
||||
/// Apple's TSS endpoint URL
|
||||
const TSS_CONTROLLER_ACTION_URL: &str = "http://gs.apple.com/TSS/controller?action=2";
|
||||
|
||||
/// Represents a TSS request to Apple's signing server
|
||||
#[derive(Debug)]
|
||||
pub struct TSSRequest {
|
||||
/// The underlying plist dictionary containing request parameters
|
||||
inner: plist::Dictionary,
|
||||
}
|
||||
|
||||
impl TSSRequest {
|
||||
/// Creates a new TSS request with default headers
|
||||
///
|
||||
/// Initializes with:
|
||||
/// - Host platform info
|
||||
/// - Client version string
|
||||
/// - Random UUID for request identification
|
||||
pub fn new() -> Self {
|
||||
let mut inner = plist::Dictionary::new();
|
||||
inner.insert("@HostPlatformInfo".into(), "mac".into());
|
||||
@@ -26,12 +40,35 @@ impl TSSRequest {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// Inserts a key-value pair into the TSS request
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - The parameter name
|
||||
/// * `val` - The parameter value (will be converted to plist::Value)
|
||||
pub fn insert(&mut self, key: impl Into<String>, val: impl Into<Value>) {
|
||||
let key = key.into();
|
||||
let val = val.into();
|
||||
self.inner.insert(key, val);
|
||||
}
|
||||
|
||||
/// Sends the TSS request to Apple's servers
|
||||
///
|
||||
/// # Returns
|
||||
/// The parsed plist response from Apple
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - The request fails
|
||||
/// - The response is malformed
|
||||
/// - Apple returns a non-success status
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let mut request = TSSRequest::new();
|
||||
/// request.insert("ApBoardID", board_id);
|
||||
/// request.insert("ApChipID", chip_id);
|
||||
/// let response = request.send().await?;
|
||||
/// ```
|
||||
pub async fn send(&self) -> Result<plist::Value, IdeviceError> {
|
||||
debug!(
|
||||
"Sending TSS request: {}",
|
||||
@@ -51,7 +88,7 @@ impl TSSRequest {
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("Apple responeded with {res}");
|
||||
debug!("Apple responded with {res}");
|
||||
let res = res.trim_start_matches("STATUS=0&");
|
||||
let res = res.trim_start_matches("MESSAGE=");
|
||||
if !res.starts_with("SUCCESS") {
|
||||
@@ -68,11 +105,24 @@ impl TSSRequest {
|
||||
}
|
||||
|
||||
impl Default for TSSRequest {
|
||||
/// Creates a default TSS request (same as `new()`)
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies restore request rules to modify input parameters
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `input` - The dictionary to modify based on rules
|
||||
/// * `parameters` - Device parameters to check conditions against
|
||||
/// * `rules` - List of rules to apply
|
||||
///
|
||||
/// # Process
|
||||
/// For each rule:
|
||||
/// 1. Checks all conditions against the parameters
|
||||
/// 2. If all conditions are met, applies the rule's actions
|
||||
/// 3. Actions can add, modify or remove parameters
|
||||
pub fn apply_restore_request_rules(
|
||||
input: &mut plist::Dictionary,
|
||||
parameters: &plist::Dictionary,
|
||||
@@ -122,6 +172,7 @@ pub fn apply_restore_request_rules(
|
||||
};
|
||||
|
||||
for (key, value) in actions {
|
||||
// Skip special values (255 typically means "ignore")
|
||||
if let Some(i) = value.as_unsigned_integer() {
|
||||
if i == 255 {
|
||||
continue;
|
||||
@@ -133,7 +184,7 @@ pub fn apply_restore_request_rules(
|
||||
}
|
||||
}
|
||||
|
||||
input.remove(key); // Explicitly remove before inserting, like Python
|
||||
input.remove(key); // Explicitly remove before inserting
|
||||
input.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
} else {
|
||||
@@ -141,3 +192,4 @@ pub fn apply_restore_request_rules(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Shim code for using pymobiledevice3's tunneld
|
||||
//! Tunneld Client Implementation
|
||||
//!
|
||||
//! Provides functionality for interacting with pymobiledevice3's tunneld service,
|
||||
//! which creates network tunnels to iOS devices over USB.
|
||||
|
||||
use std::{collections::HashMap, net::SocketAddr};
|
||||
|
||||
@@ -8,25 +11,55 @@ use serde_json::Value;
|
||||
|
||||
use crate::IdeviceError;
|
||||
|
||||
/// Default port number for the tunneld service
|
||||
pub const DEFAULT_PORT: u16 = 49151;
|
||||
|
||||
/// Represents a device connected through tunneld
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TunneldDevice {
|
||||
/// Network interface name
|
||||
pub interface: String,
|
||||
/// Tunnel IP address
|
||||
#[serde(rename = "tunnel-address")]
|
||||
pub tunnel_address: String,
|
||||
/// Tunnel port number
|
||||
#[serde(rename = "tunnel-port")]
|
||||
pub tunnel_port: u16,
|
||||
}
|
||||
|
||||
/// Retrieves all devices currently connected through tunneld
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `socket` - Socket address of the tunneld service (typically localhost with DEFAULT_PORT)
|
||||
///
|
||||
/// # Returns
|
||||
/// A HashMap mapping device UDIDs to their tunnel information
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - The HTTP request fails
|
||||
/// - The response format is invalid
|
||||
/// - JSON parsing fails
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT);
|
||||
/// let devices = get_tunneld_devices(host).await?;
|
||||
/// for (udid, device) in devices {
|
||||
/// println!("Device {} is available at {}:{}",
|
||||
/// udid, device.tunnel_address, device.tunnel_port);
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_tunneld_devices(
|
||||
socket: SocketAddr,
|
||||
) -> Result<HashMap<String, TunneldDevice>, IdeviceError> {
|
||||
// Make HTTP GET request to tunneld endpoint
|
||||
let res: Value = reqwest::get(format!("http://{socket}"))
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
// Verify response is a JSON object
|
||||
let res = match res.as_object() {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
@@ -35,6 +68,7 @@ pub async fn get_tunneld_devices(
|
||||
}
|
||||
};
|
||||
|
||||
// Parse each device entry
|
||||
let mut to_return = HashMap::new();
|
||||
for (udid, v) in res.into_iter() {
|
||||
let mut v: Vec<TunneldDevice> = match serde_json::from_value(v.clone()) {
|
||||
@@ -62,9 +96,14 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Test case for verifying tunneld device listing
|
||||
#[tokio::test]
|
||||
async fn t1() {
|
||||
async fn test_get_tunneld_devices() {
|
||||
let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT);
|
||||
println!("{:#?}", get_tunneld_devices(host).await);
|
||||
match get_tunneld_devices(host).await {
|
||||
Ok(devices) => println!("Found tunneld devices: {:#?}", devices),
|
||||
Err(e) => println!("Error querying tunneld: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Jackson Coxson
|
||||
//! USB Multiplexing Daemon (usbmuxd) Client
|
||||
//!
|
||||
//! Provides functionality for interacting with the usbmuxd service which manages
|
||||
//! connections to iOS devices over USB and network and pairing files
|
||||
|
||||
use std::{
|
||||
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
@@ -18,36 +21,57 @@ use crate::{
|
||||
mod des;
|
||||
mod raw_packet;
|
||||
|
||||
/// Represents the connection type of a device
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Connection {
|
||||
/// Connected via USB
|
||||
Usb,
|
||||
/// Connected via network with specific IP address
|
||||
Network(IpAddr),
|
||||
/// Unknown connection type with description
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
/// Represents a device connected through usbmuxd
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UsbmuxdDevice {
|
||||
/// How the device is connected
|
||||
pub connection_type: Connection,
|
||||
/// Unique Device Identifier
|
||||
pub udid: String,
|
||||
/// usbmuxd-assigned device ID
|
||||
pub device_id: u32,
|
||||
}
|
||||
|
||||
/// Active connection to the usbmuxd service
|
||||
pub struct UsbmuxdConnection {
|
||||
socket: Box<dyn ReadWrite>,
|
||||
tag: u32,
|
||||
}
|
||||
|
||||
/// Address of the usbmuxd service
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UsbmuxdAddr {
|
||||
/// Unix domain socket path (Unix systems only)
|
||||
#[cfg(unix)]
|
||||
UnixSocket(String),
|
||||
/// TCP socket address
|
||||
TcpSocket(SocketAddr),
|
||||
}
|
||||
|
||||
impl UsbmuxdAddr {
|
||||
/// Default TCP port for usbmuxd
|
||||
pub const DEFAULT_PORT: u16 = 27015;
|
||||
/// Default Unix socket path for usbmuxd
|
||||
pub const SOCKET_FILE: &'static str = "/var/run/usbmuxd";
|
||||
|
||||
/// Connects to the usbmuxd service
|
||||
///
|
||||
/// # Returns
|
||||
/// A boxed transport stream
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if connection fails
|
||||
pub async fn to_socket(&self) -> Result<Box<dyn ReadWrite>, IdeviceError> {
|
||||
Ok(match self {
|
||||
#[cfg(unix)]
|
||||
@@ -56,11 +80,24 @@ impl UsbmuxdAddr {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new usbmuxd connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `tag` - Connection tag/identifier
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `UsbmuxdConnection`
|
||||
pub async fn connect(&self, tag: u32) -> Result<UsbmuxdConnection, IdeviceError> {
|
||||
let socket = self.to_socket().await?;
|
||||
Ok(UsbmuxdConnection::new(socket, tag))
|
||||
}
|
||||
|
||||
/// Creates a UsbmuxdAddr from environment variable
|
||||
///
|
||||
/// Checks `USBMUXD_SOCKET_ADDRESS` environment variable, falls back to default
|
||||
///
|
||||
/// # Returns
|
||||
/// Configured UsbmuxdAddr or parse error
|
||||
pub fn from_env_var() -> Result<Self, AddrParseError> {
|
||||
Ok(match std::env::var("USBMUXD_SOCKET_ADDRESS") {
|
||||
Ok(var) => {
|
||||
@@ -79,6 +116,9 @@ impl UsbmuxdAddr {
|
||||
}
|
||||
|
||||
impl Default for UsbmuxdAddr {
|
||||
/// Creates default usbmuxd address based on platform:
|
||||
/// - Unix: Uses default socket path
|
||||
/// - Non-Unix: Uses localhost TCP port
|
||||
fn default() -> Self {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
@@ -93,12 +133,22 @@ impl Default for UsbmuxdAddr {
|
||||
}
|
||||
|
||||
impl UsbmuxdConnection {
|
||||
/// Binary PLIST protocol version
|
||||
pub const BINARY_PLIST_VERSION: u32 = 0;
|
||||
/// XML PLIST protocol version
|
||||
pub const XML_PLIST_VERSION: u32 = 1;
|
||||
|
||||
/// Result message type
|
||||
pub const RESULT_MESSAGE_TYPE: u32 = 1;
|
||||
/// PLIST message type
|
||||
pub const PLIST_MESSAGE_TYPE: u32 = 8;
|
||||
|
||||
/// Creates a default usbmuxd connection
|
||||
///
|
||||
/// Uses default address based on platform
|
||||
///
|
||||
/// # Returns
|
||||
/// Connected `UsbmuxdConnection` or error
|
||||
pub async fn default() -> Result<Self, IdeviceError> {
|
||||
let socket = UsbmuxdAddr::default().to_socket().await?;
|
||||
|
||||
@@ -108,10 +158,25 @@ impl UsbmuxdConnection {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new usbmuxd connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `socket` - The transport stream
|
||||
/// * `tag` - Connection tag/identifier
|
||||
pub fn new(socket: Box<dyn ReadWrite>, tag: u32) -> Self {
|
||||
Self { socket, tag }
|
||||
}
|
||||
|
||||
/// Lists all connected devices
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of connected devices
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - Communication fails
|
||||
/// - Response is malformed
|
||||
/// - Device info is incomplete
|
||||
pub async fn get_devices(&mut self) -> Result<Vec<UsbmuxdDevice>, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("MessageType".into(), "ListDevices".into());
|
||||
@@ -135,13 +200,13 @@ impl UsbmuxdConnection {
|
||||
|
||||
match addr[0] {
|
||||
0x02 => {
|
||||
// ipv4
|
||||
// IPv4
|
||||
Connection::Network(IpAddr::V4(Ipv4Addr::new(
|
||||
addr[4], addr[5], addr[6], addr[7],
|
||||
)))
|
||||
}
|
||||
0x1E => {
|
||||
// ipv6
|
||||
// IPv6
|
||||
if addr.len() < 24 {
|
||||
warn!("IPv6 address is less than 24 bytes");
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
@@ -182,6 +247,13 @@ impl UsbmuxdConnection {
|
||||
Ok(devs)
|
||||
}
|
||||
|
||||
/// Gets a specific device by UDID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `udid` - The device UDID to find
|
||||
///
|
||||
/// # Returns
|
||||
/// The matching device or error if not found
|
||||
pub async fn get_device(&mut self, udid: &str) -> Result<UsbmuxdDevice, IdeviceError> {
|
||||
let devices = self.get_devices().await?;
|
||||
match devices.into_iter().find(|x| x.udid == udid) {
|
||||
@@ -190,6 +262,13 @@ impl UsbmuxdConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the pairing record for a device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `udid` - The device UDID
|
||||
///
|
||||
/// # Returns
|
||||
/// The pairing file or error
|
||||
pub async fn get_pair_record(&mut self, udid: &str) -> Result<PairingFile, IdeviceError> {
|
||||
debug!("Getting pair record for {udid}");
|
||||
let mut req = plist::Dictionary::new();
|
||||
@@ -204,6 +283,10 @@ impl UsbmuxdConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the BUID
|
||||
///
|
||||
/// # Returns
|
||||
/// The BUID string or error
|
||||
pub async fn get_buid(&mut self) -> Result<String, IdeviceError> {
|
||||
let mut req = plist::Dictionary::new();
|
||||
req.insert("MessageType".into(), "ReadBUID".into());
|
||||
@@ -216,6 +299,15 @@ impl UsbmuxdConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to a service on the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `device_id` - usbmuxd device ID
|
||||
/// * `port` - TCP port to connect to (host byte order)
|
||||
/// * `label` - Connection label
|
||||
///
|
||||
/// # Returns
|
||||
/// An `Idevice` connection or error
|
||||
pub async fn connect_to_device(
|
||||
mut self,
|
||||
device_id: u32,
|
||||
@@ -243,6 +335,7 @@ impl UsbmuxdConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a PLIST message to usbmuxd
|
||||
async fn write_plist(&mut self, req: plist::Dictionary) -> Result<(), IdeviceError> {
|
||||
let raw = raw_packet::RawPacket::new(
|
||||
req,
|
||||
@@ -257,6 +350,7 @@ impl UsbmuxdConnection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads a PLIST message from usbmuxd
|
||||
async fn read_plist(&mut self) -> Result<plist::Dictionary, IdeviceError> {
|
||||
let mut header_buffer = [0; 16];
|
||||
self.socket.read_exact(&mut header_buffer).await?;
|
||||
@@ -276,6 +370,15 @@ impl UsbmuxdConnection {
|
||||
}
|
||||
|
||||
impl UsbmuxdDevice {
|
||||
/// Creates a provider for this device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `addr` - usbmuxd address
|
||||
/// * `tag` - Connection tag
|
||||
/// * `label` - Connection label
|
||||
///
|
||||
/// # Returns
|
||||
/// Configured `UsbmuxdProvider`
|
||||
pub fn to_provider(
|
||||
&self,
|
||||
addr: UsbmuxdAddr,
|
||||
@@ -293,3 +396,4 @@ impl UsbmuxdDevice {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
// Jackson Coxson
|
||||
//! Utility Functions
|
||||
//!
|
||||
//! Provides helper functions for working with Apple's Property List (PLIST) format,
|
||||
//! including serialization and pretty-printing utilities.
|
||||
|
||||
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);
|
||||
@@ -10,10 +30,32 @@ pub fn plist_to_xml_bytes(p: &plist::Dictionary) -> Vec<u8> {
|
||||
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()
|
||||
@@ -22,6 +64,14 @@ pub fn pretty_print_dictionary(dict: &plist::Dictionary) -> String {
|
||||
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 {
|
||||
@@ -75,3 +125,4 @@ fn print_plist(p: &Value, indentation: usize) -> String {
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// DebianArch
|
||||
|
||||
use json::JsonValue;
|
||||
pub struct CDTunnel {}
|
||||
|
||||
impl CDTunnel {
|
||||
const MAGIC: &'static [u8; 8] = b"CDTunnel";
|
||||
pub fn decode(data: &[u8]) -> Result<JsonValue, Box<dyn std::error::Error>> {
|
||||
let magic_len = CDTunnel::MAGIC.len();
|
||||
if &data[0..magic_len] != CDTunnel::MAGIC {
|
||||
Err("Invalid Magic")?;
|
||||
}
|
||||
|
||||
let size = u16::from_be_bytes(data[magic_len..magic_len + 2].try_into()?) as usize;
|
||||
let content = &data[magic_len + 2..magic_len + 2 + size];
|
||||
Ok(json::parse(&String::from_utf8(content.to_vec())?)?)
|
||||
}
|
||||
|
||||
pub fn encode(value: JsonValue) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut buf = Vec::new();
|
||||
let json_str = value.dump();
|
||||
|
||||
buf.extend_from_slice(CDTunnel::MAGIC);
|
||||
buf.extend_from_slice(&u16::to_be_bytes(json_str.len().try_into()?));
|
||||
buf.extend_from_slice(json_str.as_bytes());
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
// Thanks DebianArch
|
||||
//! XPC (Cross-Process Communication) Implementation
|
||||
//!
|
||||
//! Provides functionality for interacting with Apple's XPC protocol over HTTP/2,
|
||||
//! which is used for inter-process communication between iOS/macOS components.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -14,24 +17,33 @@ use format::{XPCFlag, XPCMessage, XPCObject};
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod cdtunnel;
|
||||
pub mod error;
|
||||
pub mod format;
|
||||
mod format;
|
||||
|
||||
/// Represents an XPC connection to a device with available services
|
||||
pub struct XPCDevice<R: ReadWrite> {
|
||||
/// The underlying XPC connection
|
||||
pub connection: XPCConnection<R>,
|
||||
/// Map of available XPC services by name
|
||||
pub services: HashMap<String, XPCService>,
|
||||
}
|
||||
|
||||
/// Describes an available XPC service
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct XPCService {
|
||||
/// Required entitlement to access this service
|
||||
pub entitlement: String,
|
||||
/// Port number where the service is available
|
||||
pub port: u16,
|
||||
/// Whether the service uses remote XPC
|
||||
pub uses_remote_xpc: bool,
|
||||
/// Optional list of supported features
|
||||
pub features: Option<Vec<String>>,
|
||||
/// Optional service version number
|
||||
pub service_version: Option<i64>,
|
||||
}
|
||||
|
||||
/// Manages an active XPC connection over HTTP/2
|
||||
pub struct XPCConnection<R: ReadWrite> {
|
||||
pub(crate) inner: http2::Connection<R>,
|
||||
root_message_id: u64,
|
||||
@@ -39,9 +51,22 @@ pub struct XPCConnection<R: ReadWrite> {
|
||||
}
|
||||
|
||||
impl<R: ReadWrite> XPCDevice<R> {
|
||||
/// Creates a new XPC device connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stream` - The underlying transport stream
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected XPCDevice instance with discovered services
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if:
|
||||
/// - The connection fails
|
||||
/// - The service discovery response is malformed
|
||||
pub async fn new(stream: R) -> Result<Self, IdeviceError> {
|
||||
let mut connection = XPCConnection::new(stream).await?;
|
||||
|
||||
// Read initial services message
|
||||
let data = connection.read_message(http2::ROOT_CHANNEL).await?;
|
||||
|
||||
let data = match data.message {
|
||||
@@ -56,6 +81,7 @@ impl<R: ReadWrite> XPCDevice<R> {
|
||||
None => return Err(IdeviceError::UnexpectedResponse),
|
||||
};
|
||||
|
||||
// Parse available services
|
||||
let mut services = HashMap::new();
|
||||
for (name, service) in data.into_iter() {
|
||||
match service.as_dictionary() {
|
||||
@@ -131,18 +157,34 @@ impl<R: ReadWrite> XPCDevice<R> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Consumes the device and returns the underlying transport stream
|
||||
pub fn into_inner(self) -> R {
|
||||
self.connection.inner.stream
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: ReadWrite> XPCConnection<R> {
|
||||
/// Channel ID for root messages
|
||||
pub const ROOT_CHANNEL: u32 = http2::ROOT_CHANNEL;
|
||||
/// Channel ID for reply messages
|
||||
pub const REPLY_CHANNEL: u32 = http2::REPLY_CHANNEL;
|
||||
/// Initial stream ID for HTTP/2 connection
|
||||
const INIT_STREAM: u32 = http2::INIT_STREAM;
|
||||
|
||||
/// Establishes a new XPC connection
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stream` - The underlying transport stream
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected XPCConnection instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `XPCError` if the connection handshake fails
|
||||
pub async fn new(stream: R) -> Result<Self, XPCError> {
|
||||
let mut client = http2::Connection::new(stream).await?;
|
||||
|
||||
// Configure HTTP/2 settings
|
||||
client
|
||||
.send_frame(SettingsFrame::new(
|
||||
[
|
||||
@@ -154,14 +196,19 @@ impl<R: ReadWrite> XPCConnection<R> {
|
||||
Default::default(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// Update window size
|
||||
client
|
||||
.send_frame(WindowUpdateFrame::new(Self::INIT_STREAM, 983041))
|
||||
.await?;
|
||||
|
||||
let mut xpc_client = Self {
|
||||
inner: client,
|
||||
root_message_id: 1,
|
||||
reply_message_id: 1,
|
||||
};
|
||||
|
||||
// Perform XPC handshake
|
||||
xpc_client
|
||||
.send_recv_message(
|
||||
Self::ROOT_CHANNEL,
|
||||
@@ -173,7 +220,6 @@ impl<R: ReadWrite> XPCConnection<R> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// we are here. we send data to stream_id 3 yet we get data from stream 1 ???
|
||||
xpc_client
|
||||
.send_recv_message(
|
||||
Self::REPLY_CHANNEL,
|
||||
@@ -195,6 +241,14 @@ impl<R: ReadWrite> XPCConnection<R> {
|
||||
Ok(xpc_client)
|
||||
}
|
||||
|
||||
/// Sends a message and waits for the response
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stream_id` - The channel/stream to use
|
||||
/// * `message` - The XPC message to send
|
||||
///
|
||||
/// # Returns
|
||||
/// The response message
|
||||
pub async fn send_recv_message(
|
||||
&mut self,
|
||||
stream_id: u32,
|
||||
@@ -204,6 +258,7 @@ impl<R: ReadWrite> XPCConnection<R> {
|
||||
self.read_message(stream_id).await
|
||||
}
|
||||
|
||||
/// Sends an XPC message without waiting for a response
|
||||
pub async fn send_message(
|
||||
&mut self,
|
||||
stream_id: u32,
|
||||
@@ -215,6 +270,7 @@ impl<R: ReadWrite> XPCConnection<R> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads an XPC message from the specified stream
|
||||
pub async fn read_message(&mut self, stream_id: u32) -> Result<XPCMessage, XPCError> {
|
||||
let mut buf = self.inner.read_streamid(stream_id).await?;
|
||||
loop {
|
||||
@@ -236,3 +292,4 @@ impl<R: ReadWrite> XPCConnection<R> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// idevice Rust implementation of libimobiledevice's ideviceinfo
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{lockdown::LockdowndClient, IdeviceService};
|
||||
use idevice::{lockdown::LockdownClient, IdeviceService};
|
||||
|
||||
mod common;
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let mut lockdown_client = match LockdowndClient::connect(&*provider).await {
|
||||
let mut lockdown_client = match LockdownClient::connect(&*provider).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to connect to lockdown: {e:?}");
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{io::Write, path::PathBuf};
|
||||
|
||||
use clap::{arg, value_parser, Arg, Command};
|
||||
use idevice::{
|
||||
lockdown::LockdowndClient, mobile_image_mounter::ImageMounter, pretty_print_plist,
|
||||
lockdown::LockdownClient, mobile_image_mounter::ImageMounter, pretty_print_plist,
|
||||
IdeviceService,
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let mut lockdown_client = LockdowndClient::connect(&*provider)
|
||||
let mut lockdown_client = LockdownClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Unable to connect to lockdown");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user