mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Implement basic debug proxy support
This commit is contained in:
@@ -37,6 +37,7 @@ sha2 = { version = "0.10", optional = true }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
||||||
|
debug_proxy = []
|
||||||
heartbeat = []
|
heartbeat = []
|
||||||
installation_proxy = []
|
installation_proxy = []
|
||||||
mounter = ["dep:sha2"]
|
mounter = ["dep:sha2"]
|
||||||
@@ -54,6 +55,7 @@ xpc = [
|
|||||||
]
|
]
|
||||||
full = [
|
full = [
|
||||||
"core_device_proxy",
|
"core_device_proxy",
|
||||||
|
"debug_proxy",
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
"installation_proxy",
|
"installation_proxy",
|
||||||
"mounter",
|
"mounter",
|
||||||
|
|||||||
196
idevice/src/debug_proxy.rs
Normal file
196
idevice/src/debug_proxy.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
use crate::{IdeviceError, ReadWrite};
|
||||||
|
|
||||||
|
pub const SERVICE_NAME: &str = "com.apple.internal.dt.remote.debugproxy";
|
||||||
|
|
||||||
|
pub struct DebugProxyClient {
|
||||||
|
pub socket: Box<dyn ReadWrite>,
|
||||||
|
pub noack_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DebugserverCommand {
|
||||||
|
pub name: String,
|
||||||
|
pub argv: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugserverCommand {
|
||||||
|
pub fn new(name: String, argv: Vec<String>) -> Self {
|
||||||
|
Self { name, argv }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugProxyClient {
|
||||||
|
pub fn new(socket: Box<dyn ReadWrite>) -> Self {
|
||||||
|
Self {
|
||||||
|
socket,
|
||||||
|
noack_mode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_command(
|
||||||
|
&mut self,
|
||||||
|
command: DebugserverCommand,
|
||||||
|
) -> Result<Option<String>, IdeviceError> {
|
||||||
|
// Hex-encode the arguments
|
||||||
|
let hex_args = command
|
||||||
|
.argv
|
||||||
|
.iter()
|
||||||
|
.map(|arg| hex_encode(arg.as_bytes()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Construct the packet data (command + hex-encoded arguments)
|
||||||
|
let packet_data = format!("{}{}", command.name, hex_args);
|
||||||
|
|
||||||
|
// Calculate the checksum
|
||||||
|
let checksum = calculate_checksum(&packet_data);
|
||||||
|
|
||||||
|
// Construct the full packet
|
||||||
|
let packet = format!("${}#{}", packet_data, checksum);
|
||||||
|
|
||||||
|
// Log the packet for debugging
|
||||||
|
debug!("Sending packet: {}", packet);
|
||||||
|
|
||||||
|
// Send the packet
|
||||||
|
self.socket.write_all(packet.as_bytes()).await?;
|
||||||
|
|
||||||
|
// Read the response
|
||||||
|
let response = self.read_response().await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_response(&mut self) -> Result<Option<String>, IdeviceError> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let mut received_char = [0u8; 1];
|
||||||
|
|
||||||
|
if !self.noack_mode {
|
||||||
|
self.socket.read_exact(&mut received_char).await?;
|
||||||
|
if received_char[0] != b'+' {
|
||||||
|
debug!("No + ack");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.socket.read_exact(&mut received_char).await?;
|
||||||
|
if received_char[0] != b'$' {
|
||||||
|
debug!("No $ response");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
self.socket.read_exact(&mut received_char).await?;
|
||||||
|
if received_char[0] == b'#' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buffer.push(received_char[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.noack_mode {
|
||||||
|
self.send_ack().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = String::from_utf8(buffer)?;
|
||||||
|
Ok(Some(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> {
|
||||||
|
self.socket.write_all(bytes).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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?;
|
||||||
|
|
||||||
|
Ok(String::from_utf8_lossy(&buf[..r]).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_argv(&mut self, argv: Vec<String>) -> Result<String, IdeviceError> {
|
||||||
|
if argv.is_empty() {
|
||||||
|
return Err(IdeviceError::InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the total length of the packet
|
||||||
|
let mut pkt_len = 0;
|
||||||
|
for (i, arg) in argv.iter().enumerate() {
|
||||||
|
let prefix = format!(",{},{},", arg.len() * 2, i);
|
||||||
|
pkt_len += prefix.len() + arg.len() * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate and initialize the packet
|
||||||
|
let mut pkt = vec![0u8; pkt_len + 1];
|
||||||
|
let mut pktp = 0;
|
||||||
|
|
||||||
|
for (i, arg) in argv.iter().enumerate() {
|
||||||
|
let prefix = format!(",{},{},", arg.len() * 2, i);
|
||||||
|
let prefix_bytes = prefix.as_bytes();
|
||||||
|
|
||||||
|
// Copy prefix to the packet
|
||||||
|
pkt[pktp..pktp + prefix_bytes.len()].copy_from_slice(prefix_bytes);
|
||||||
|
pktp += prefix_bytes.len();
|
||||||
|
|
||||||
|
// Hex encode the argument
|
||||||
|
for byte in arg.bytes() {
|
||||||
|
let hex = format!("{:02X}", byte);
|
||||||
|
pkt[pktp..pktp + 2].copy_from_slice(hex.as_bytes());
|
||||||
|
pktp += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the first byte of the packet
|
||||||
|
pkt[0] = b'A';
|
||||||
|
|
||||||
|
// Simulate sending the command and receiving a response
|
||||||
|
self.send_raw(&pkt).await?;
|
||||||
|
let response = self.read(16).await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_ack(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
self.socket.write_all(b"+").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_noack(&mut self) -> Result<(), IdeviceError> {
|
||||||
|
self.socket.write_all(b"-").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ack_mode(&mut self, enabled: bool) {
|
||||||
|
self.noack_mode = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_checksum(data: &str) -> String {
|
||||||
|
let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte));
|
||||||
|
format!("{:02x}", checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_encode(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().fold(String::new(), |mut output, b| {
|
||||||
|
let _ = write!(output, "{b:02X}");
|
||||||
|
output
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for DebugserverCommand {
|
||||||
|
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 {
|
||||||
|
fn from(s: &str) -> DebugserverCommand {
|
||||||
|
s.to_string().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#[cfg(feature = "core_device_proxy")]
|
#[cfg(feature = "core_device_proxy")]
|
||||||
pub mod core_device_proxy;
|
pub mod core_device_proxy;
|
||||||
|
#[cfg(feature = "debug_proxy")]
|
||||||
|
pub mod debug_proxy;
|
||||||
#[cfg(feature = "heartbeat")]
|
#[cfg(feature = "heartbeat")]
|
||||||
pub mod heartbeat;
|
pub mod heartbeat;
|
||||||
#[cfg(feature = "xpc")]
|
#[cfg(feature = "xpc")]
|
||||||
@@ -294,6 +296,10 @@ pub enum IdeviceError {
|
|||||||
#[error("xpc message failed")]
|
#[error("xpc message failed")]
|
||||||
Xpc(#[from] xpc::error::XPCError),
|
Xpc(#[from] xpc::error::XPCError),
|
||||||
|
|
||||||
|
#[cfg(feature = "debug_proxy")]
|
||||||
|
#[error("invalid argument passed")]
|
||||||
|
InvalidArgument,
|
||||||
|
|
||||||
#[error("unknown error `{0}` returned from device")]
|
#[error("unknown error `{0}` returned from device")]
|
||||||
UnknownErrorType(String),
|
UnknownErrorType(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ impl XPCConnection {
|
|||||||
return Ok(decoded);
|
return Ok(decoded);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Error decoding message: {:?}", err);
|
log::warn!("Error decoding message: {:?}", err);
|
||||||
buf.extend_from_slice(&self.inner.read_streamid(stream_id).await?);
|
buf.extend_from_slice(&self.inner.read_streamid(stream_id).await?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ path = "src/idevice_id.rs"
|
|||||||
name = "remotexpc"
|
name = "remotexpc"
|
||||||
path = "src/remotexpc.rs"
|
path = "src/remotexpc.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "debug_proxy"
|
||||||
|
path = "src/debug_proxy.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idevice = { path = "../idevice", features = ["full"] }
|
idevice = { path = "../idevice", features = ["full"] }
|
||||||
|
|||||||
102
tools/src/debug_proxy.rs
Normal file
102
tools/src/debug_proxy.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use idevice::{debug_proxy::DebugProxyClient, tunneld::get_tunneld_devices, xpc::XPCDevice};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let matches = Command::new("remotexpc")
|
||||||
|
.about("Get services from RemoteXPC")
|
||||||
|
.arg(
|
||||||
|
Arg::new("udid")
|
||||||
|
.value_name("UDID")
|
||||||
|
.help("UDID of the device (overrides host/pairing file)")
|
||||||
|
.index(1),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("about")
|
||||||
|
.long("about")
|
||||||
|
.help("Show about information")
|
||||||
|
.action(clap::ArgAction::SetTrue),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
if matches.get_flag("about") {
|
||||||
|
println!("debug_proxy - connect to the debug proxy and run commands");
|
||||||
|
println!("Copyright (c) 2025 Jackson Coxson");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let udid = matches.get_one::<String>("udid");
|
||||||
|
|
||||||
|
let socket = SocketAddr::new(
|
||||||
|
IpAddr::from_str("127.0.0.1").unwrap(),
|
||||||
|
idevice::tunneld::DEFAULT_PORT,
|
||||||
|
);
|
||||||
|
let mut devices = get_tunneld_devices(socket)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get tunneld devices");
|
||||||
|
|
||||||
|
let (_udid, device) = match udid {
|
||||||
|
Some(u) => (
|
||||||
|
u.to_owned(),
|
||||||
|
devices.remove(u).expect("Device not in tunneld"),
|
||||||
|
),
|
||||||
|
None => devices.into_iter().next().expect("No devices"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the connection to RemoteXPC
|
||||||
|
let client = XPCDevice::new(Box::new(
|
||||||
|
TcpStream::connect((device.tunnel_address.as_str(), device.tunnel_port))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get the debug proxy
|
||||||
|
let service = client
|
||||||
|
.services
|
||||||
|
.get(idevice::debug_proxy::SERVICE_NAME)
|
||||||
|
.expect("Client did not contain debug proxy service");
|
||||||
|
|
||||||
|
let stream = TcpStream::connect(SocketAddr::new(
|
||||||
|
IpAddr::from_str(&device.tunnel_address).unwrap(),
|
||||||
|
service.port,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect");
|
||||||
|
|
||||||
|
let mut dp = DebugProxyClient::new(Box::new(stream));
|
||||||
|
|
||||||
|
println!("Shell connected!");
|
||||||
|
loop {
|
||||||
|
print!("> ");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
std::io::stdin().read_line(&mut buf).unwrap();
|
||||||
|
|
||||||
|
let buf = buf.trim();
|
||||||
|
|
||||||
|
if buf == "exit" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = dp.send_command(buf.into()).await.expect("Failed to send");
|
||||||
|
if let Some(res) = res {
|
||||||
|
println!("{res}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user