mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Initial afc support
This commit is contained in:
@@ -43,6 +43,7 @@ tun-rs = { version = "2.0.8", features = ["async_tokio"] }
|
||||
bytes = "1.10.1"
|
||||
|
||||
[features]
|
||||
afc = []
|
||||
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
||||
debug_proxy = []
|
||||
dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
|
||||
@@ -66,6 +67,7 @@ xpc = [
|
||||
"dep:json",
|
||||
]
|
||||
full = [
|
||||
"afc",
|
||||
"core_device_proxy",
|
||||
"debug_proxy",
|
||||
"dvt",
|
||||
@@ -80,7 +82,7 @@ full = [
|
||||
"tunnel_tcp_stack",
|
||||
"tss",
|
||||
"tunneld",
|
||||
"sbservices"
|
||||
"sbservices",
|
||||
]
|
||||
|
||||
# Why: https://github.com/rust-lang/cargo/issues/1197
|
||||
|
||||
107
idevice/src/afc/errors.rs
Normal file
107
idevice/src/afc/errors.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
// Jackson Coxson
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
#[repr(C)]
|
||||
pub enum AfcError {
|
||||
Success = 0,
|
||||
UnknownError = 1,
|
||||
OpHeaderInvalid = 2,
|
||||
NoResources = 3,
|
||||
ReadError = 4,
|
||||
WriteError = 5,
|
||||
UnknownPacketType = 6,
|
||||
InvalidArg = 7,
|
||||
ObjectNotFound = 8,
|
||||
ObjectIsDir = 9,
|
||||
PermDenied = 10,
|
||||
ServiceNotConnected = 11,
|
||||
OpTimeout = 12,
|
||||
TooMuchData = 13,
|
||||
EndOfData = 14,
|
||||
OpNotSupported = 15,
|
||||
ObjectExists = 16,
|
||||
ObjectBusy = 17,
|
||||
NoSpaceLeft = 18,
|
||||
OpWouldBlock = 19,
|
||||
IoError = 20,
|
||||
OpInterrupted = 21,
|
||||
OpInProgress = 22,
|
||||
InternalError = 23,
|
||||
MuxError = 30,
|
||||
NoMem = 31,
|
||||
NotEnoughData = 32,
|
||||
DirNotEmpty = 33,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AfcError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let description = match self {
|
||||
AfcError::Success => "Success",
|
||||
AfcError::UnknownError => "Unknown error",
|
||||
AfcError::OpHeaderInvalid => "Operation header invalid",
|
||||
AfcError::NoResources => "No resources available",
|
||||
AfcError::ReadError => "Read error",
|
||||
AfcError::WriteError => "Write error",
|
||||
AfcError::UnknownPacketType => "Unknown packet type",
|
||||
AfcError::InvalidArg => "Invalid argument",
|
||||
AfcError::ObjectNotFound => "Object not found",
|
||||
AfcError::ObjectIsDir => "Object is a directory",
|
||||
AfcError::PermDenied => "Permission denied",
|
||||
AfcError::ServiceNotConnected => "Service not connected",
|
||||
AfcError::OpTimeout => "Operation timed out",
|
||||
AfcError::TooMuchData => "Too much data",
|
||||
AfcError::EndOfData => "End of data",
|
||||
AfcError::OpNotSupported => "Operation not supported",
|
||||
AfcError::ObjectExists => "Object already exists",
|
||||
AfcError::ObjectBusy => "Object is busy",
|
||||
AfcError::NoSpaceLeft => "No space left",
|
||||
AfcError::OpWouldBlock => "Operation would block",
|
||||
AfcError::IoError => "I/O error",
|
||||
AfcError::OpInterrupted => "Operation interrupted",
|
||||
AfcError::OpInProgress => "Operation in progress",
|
||||
AfcError::InternalError => "Internal error",
|
||||
AfcError::MuxError => "Multiplexer error",
|
||||
AfcError::NoMem => "Out of memory",
|
||||
AfcError::NotEnoughData => "Not enough data",
|
||||
AfcError::DirNotEmpty => "Directory not empty",
|
||||
};
|
||||
write!(f, "{}", description)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for AfcError {
|
||||
fn from(value: u64) -> Self {
|
||||
match value {
|
||||
0 => Self::Success,
|
||||
1 => Self::UnknownError,
|
||||
2 => Self::OpHeaderInvalid,
|
||||
3 => Self::NoResources,
|
||||
4 => Self::ReadError,
|
||||
5 => Self::WriteError,
|
||||
6 => Self::UnknownPacketType,
|
||||
7 => Self::InvalidArg,
|
||||
8 => Self::ObjectNotFound,
|
||||
9 => Self::ObjectIsDir,
|
||||
10 => Self::PermDenied,
|
||||
11 => Self::ServiceNotConnected,
|
||||
12 => Self::OpTimeout,
|
||||
13 => Self::TooMuchData,
|
||||
14 => Self::EndOfData,
|
||||
15 => Self::OpNotSupported,
|
||||
16 => Self::ObjectExists,
|
||||
17 => Self::ObjectBusy,
|
||||
18 => Self::NoSpaceLeft,
|
||||
19 => Self::OpWouldBlock,
|
||||
20 => Self::IoError,
|
||||
21 => Self::OpInterrupted,
|
||||
22 => Self::OpInProgress,
|
||||
23 => Self::InternalError,
|
||||
30 => Self::MuxError,
|
||||
31 => Self::NoMem,
|
||||
32 => Self::NotEnoughData,
|
||||
33 => Self::DirNotEmpty,
|
||||
_ => Self::UnknownError, // fallback for unknown codes
|
||||
}
|
||||
}
|
||||
}
|
||||
107
idevice/src/afc/mod.rs
Normal file
107
idevice/src/afc/mod.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use errors::AfcError;
|
||||
use opcode::AfcOpcode;
|
||||
use packet::{AfcPacket, AfcPacketHeader};
|
||||
|
||||
use crate::{lockdownd::LockdowndClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
pub mod errors;
|
||||
pub mod opcode;
|
||||
pub mod packet;
|
||||
|
||||
pub const MAGIC: u64 = 0x4141504c36414643;
|
||||
|
||||
pub struct AfcClient {
|
||||
pub idevice: Idevice,
|
||||
package_number: u64,
|
||||
}
|
||||
|
||||
impl IdeviceService for AfcClient {
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.afc"
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdowndClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
|
||||
let (port, ssl) = lockdown.start_service(Self::service_name()).await?;
|
||||
|
||||
let mut idevice = provider.connect(port).await?;
|
||||
if ssl {
|
||||
idevice
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
idevice,
|
||||
package_number: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AfcClient {
|
||||
pub fn new(idevice: Idevice) -> Self {
|
||||
Self {
|
||||
idevice,
|
||||
package_number: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&mut self, path: impl Into<String>) -> Result<Vec<String>, IdeviceError> {
|
||||
let path = path.into();
|
||||
let header_payload = path.as_bytes().to_vec();
|
||||
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
|
||||
|
||||
let header = AfcPacketHeader {
|
||||
magic: MAGIC,
|
||||
entire_len: header_len, // it's the same since the payload is empty for this
|
||||
header_payload_len: header_len,
|
||||
packet_num: self.package_number,
|
||||
operation: AfcOpcode::ReadDir,
|
||||
};
|
||||
self.package_number += 1;
|
||||
|
||||
let packet = AfcPacket {
|
||||
header,
|
||||
header_payload,
|
||||
payload: Vec::new(),
|
||||
};
|
||||
|
||||
self.send(packet).await?;
|
||||
let res = self.read().await?;
|
||||
|
||||
let strings: Vec<String> = res
|
||||
.payload
|
||||
.split(|b| *b == 0)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| String::from_utf8_lossy(s).into_owned())
|
||||
.collect();
|
||||
Ok(strings)
|
||||
}
|
||||
|
||||
pub async fn read(&mut self) -> Result<AfcPacket, IdeviceError> {
|
||||
let res = AfcPacket::read(&mut self.idevice).await?;
|
||||
if res.header.operation == AfcOpcode::Status {
|
||||
if res.header_payload.len() < 8 {
|
||||
log::error!("AFC returned error opcode, but not a code");
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
let code = u64::from_le_bytes(res.header_payload[..8].try_into().unwrap());
|
||||
return Err(IdeviceError::Afc(AfcError::from(code)));
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, packet: AfcPacket) -> Result<(), IdeviceError> {
|
||||
let packet = packet.serialize();
|
||||
self.idevice.send_raw(&packet).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
83
idevice/src/afc/opcode.rs
Normal file
83
idevice/src/afc/opcode.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
// Jackson Coxson
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum AfcOpcode {
|
||||
Status = 0x00000001,
|
||||
Data = 0x00000002, // Data
|
||||
ReadDir = 0x00000003, // ReadDir
|
||||
ReadFile = 0x00000004, // ReadFile
|
||||
WriteFile = 0x00000005, // WriteFile
|
||||
WritePart = 0x00000006, // WritePart
|
||||
Truncate = 0x00000007, // TruncateFile
|
||||
RemovePath = 0x00000008, // RemovePath
|
||||
MakeDir = 0x00000009, // MakeDir
|
||||
GetFileInfo = 0x0000000a, // GetFileInfo
|
||||
GetDevInfo = 0x0000000b, // GetDeviceInfo
|
||||
WriteFileAtom = 0x0000000c, // WriteFileAtomic (tmp file+rename)
|
||||
FileOpen = 0x0000000d, // FileRefOpen
|
||||
FileOpenRes = 0x0000000e, // FileRefOpenResult
|
||||
Read = 0x0000000f, // FileRefRead
|
||||
Write = 0x00000010, // FileRefWrite
|
||||
FileSeek = 0x00000011, // FileRefSeek
|
||||
FileTell = 0x00000012, // FileRefTell
|
||||
FileTellRes = 0x00000013, // FileRefTellResult
|
||||
FileClose = 0x00000014, // FileRefClose
|
||||
FileSetSize = 0x00000015, // FileRefSetFileSize (ftruncate)
|
||||
GetConInfo = 0x00000016, // GetConnectionInfo
|
||||
SetConOptions = 0x00000017, // SetConnectionOptions
|
||||
RenamePath = 0x00000018, // RenamePath
|
||||
SetFsBs = 0x00000019, // SetFSBlockSize (0x800000)
|
||||
SetSocketBs = 0x0000001A, // SetSocketBlockSize (0x800000)
|
||||
FileLock = 0x0000001B, // FileRefLock
|
||||
MakeLink = 0x0000001C, // MakeLink
|
||||
SetFileTime = 0x0000001E, // Set st_mtime
|
||||
}
|
||||
|
||||
pub enum AfcFopenMode {
|
||||
RdOnly = 0x00000001, // r O_RDONLY
|
||||
Rw = 0x00000002, // r+ O_RDWR | O_CREAT
|
||||
WrOnly = 0x00000003, // w O_WRONLY | O_CREAT | O_TRUNC
|
||||
Wr = 0x00000004, // w+ O_RDWR | O_CREAT | O_TRUNC
|
||||
Append = 0x00000005, // a O_WRONLY | O_APPEND | O_CREAT
|
||||
RdAppend = 0x00000006, // a+ O_RDWR | O_APPEND | O_CREAT
|
||||
}
|
||||
|
||||
impl TryFrom<u64> for AfcOpcode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u64) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00000001 => Ok(Self::Status),
|
||||
0x00000002 => Ok(Self::Data),
|
||||
0x00000003 => Ok(Self::ReadDir),
|
||||
0x00000004 => Ok(Self::ReadFile),
|
||||
0x00000005 => Ok(Self::WriteFile),
|
||||
0x00000006 => Ok(Self::WritePart),
|
||||
0x00000007 => Ok(Self::Truncate),
|
||||
0x00000008 => Ok(Self::RemovePath),
|
||||
0x00000009 => Ok(Self::MakeDir),
|
||||
0x0000000a => Ok(Self::GetFileInfo),
|
||||
0x0000000b => Ok(Self::GetDevInfo),
|
||||
0x0000000c => Ok(Self::WriteFileAtom),
|
||||
0x0000000d => Ok(Self::FileOpen),
|
||||
0x0000000e => Ok(Self::FileOpenRes),
|
||||
0x0000000f => Ok(Self::Read),
|
||||
0x00000010 => Ok(Self::Write),
|
||||
0x00000011 => Ok(Self::FileSeek),
|
||||
0x00000012 => Ok(Self::FileTell),
|
||||
0x00000013 => Ok(Self::FileTellRes),
|
||||
0x00000014 => Ok(Self::FileClose),
|
||||
0x00000015 => Ok(Self::FileSetSize),
|
||||
0x00000016 => Ok(Self::GetConInfo),
|
||||
0x00000017 => Ok(Self::SetConOptions),
|
||||
0x00000018 => Ok(Self::RenamePath),
|
||||
0x00000019 => Ok(Self::SetFsBs),
|
||||
0x0000001A => Ok(Self::SetSocketBs),
|
||||
0x0000001B => Ok(Self::FileLock),
|
||||
0x0000001C => Ok(Self::MakeLink),
|
||||
0x0000001E => Ok(Self::SetFileTime),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
98
idevice/src/afc/packet.rs
Normal file
98
idevice/src/afc/packet.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use log::debug;
|
||||
|
||||
use crate::{Idevice, IdeviceError};
|
||||
|
||||
use super::opcode::AfcOpcode;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AfcPacketHeader {
|
||||
pub magic: u64,
|
||||
pub entire_len: u64,
|
||||
pub header_payload_len: u64,
|
||||
pub packet_num: u64,
|
||||
pub operation: AfcOpcode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AfcPacket {
|
||||
pub header: AfcPacketHeader,
|
||||
pub header_payload: Vec<u8>,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl AfcPacketHeader {
|
||||
pub const LEN: u64 = 40;
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::with_capacity(Self::LEN as usize);
|
||||
|
||||
res.extend_from_slice(&self.magic.to_le_bytes());
|
||||
res.extend_from_slice(&self.entire_len.to_le_bytes());
|
||||
res.extend_from_slice(&self.header_payload_len.to_le_bytes());
|
||||
res.extend_from_slice(&self.packet_num.to_le_bytes());
|
||||
res.extend_from_slice(&(self.operation.clone() as u64).to_le_bytes());
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn read(reader: &mut Idevice) -> Result<Self, IdeviceError> {
|
||||
let header_bytes = reader.read_raw(Self::LEN as usize).await?;
|
||||
let mut chunks = header_bytes.chunks_exact(8);
|
||||
let res = Self {
|
||||
magic: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
|
||||
entire_len: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
|
||||
header_payload_len: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
|
||||
packet_num: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
|
||||
operation: match AfcOpcode::try_from(u64::from_le_bytes(
|
||||
chunks.next().unwrap().try_into().unwrap(),
|
||||
)) {
|
||||
Ok(o) => o,
|
||||
Err(_) => {
|
||||
return Err(IdeviceError::UnknownAfcOpcode);
|
||||
}
|
||||
},
|
||||
};
|
||||
if res.magic != super::MAGIC {
|
||||
return Err(IdeviceError::InvalidAfcMagic);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl AfcPacket {
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
res.extend_from_slice(&self.header.serialize());
|
||||
res.extend_from_slice(&self.header_payload);
|
||||
res.extend_from_slice(&self.payload);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn read(reader: &mut Idevice) -> Result<Self, IdeviceError> {
|
||||
let header = AfcPacketHeader::read(reader).await?;
|
||||
debug!("afc header: {header:?}");
|
||||
let header_payload = reader
|
||||
.read_raw((header.header_payload_len - AfcPacketHeader::LEN) as usize)
|
||||
.await?;
|
||||
|
||||
let payload = if header.header_payload_len == header.entire_len {
|
||||
Vec::new() // no payload
|
||||
} else {
|
||||
reader
|
||||
.read_raw((header.entire_len - header.header_payload_len) as usize)
|
||||
.await?
|
||||
};
|
||||
|
||||
let res = Self {
|
||||
header,
|
||||
header_payload,
|
||||
payload,
|
||||
};
|
||||
debug!("Recv afc: {res:?}");
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Jackson Coxson
|
||||
|
||||
#[cfg(feature = "afc")]
|
||||
pub mod afc;
|
||||
#[cfg(feature = "core_device_proxy")]
|
||||
pub mod core_device_proxy;
|
||||
#[cfg(feature = "debug_proxy")]
|
||||
@@ -298,6 +300,18 @@ pub enum IdeviceError {
|
||||
#[error("misagent operation failed")]
|
||||
MisagentFailure,
|
||||
|
||||
#[cfg(feature = "afc")]
|
||||
#[error("afc error")]
|
||||
Afc(#[from] afc::errors::AfcError),
|
||||
|
||||
#[cfg(feature = "afc")]
|
||||
#[error("unknown afc opcode")]
|
||||
UnknownAfcOpcode,
|
||||
|
||||
#[cfg(feature = "afc")]
|
||||
#[error("invalid afc magic")]
|
||||
InvalidAfcMagic,
|
||||
|
||||
#[cfg(any(feature = "tss", feature = "tunneld"))]
|
||||
#[error("http reqwest error")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
@@ -57,6 +57,11 @@ path = "src/misagent.rs"
|
||||
name = "location_simulation"
|
||||
path = "src/location_simulation.rs"
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "afc"
|
||||
path = "src/afc.rs"
|
||||
|
||||
[dependencies]
|
||||
idevice = { path = "../idevice", features = ["full"] }
|
||||
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }
|
||||
|
||||
81
tools/src/afc.rs
Normal file
81
tools/src/afc.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{arg, value_parser, Arg, Command};
|
||||
use idevice::{afc::AfcClient, misagent::MisagentClient, IdeviceService};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("afc")
|
||||
.about("Start a tunnel")
|
||||
.arg(
|
||||
Arg::new("host")
|
||||
.long("host")
|
||||
.value_name("HOST")
|
||||
.help("IP address of the device"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pairing_file")
|
||||
.long("pairing-file")
|
||||
.value_name("PATH")
|
||||
.help("Path to the pairing file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("udid")
|
||||
.value_name("UDID")
|
||||
.help("UDID of the device (overrides host/pairing file)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("list")
|
||||
.about("Lists the items in the directory")
|
||||
.arg(Arg::new("path").required(true).index(1)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("remove")
|
||||
.about("Remove a provisioning profile")
|
||||
.arg(Arg::new("path").required(true).index(1)),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("afc");
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
|
||||
let udid = matches.get_one::<String>("udid");
|
||||
let host = matches.get_one::<String>("host");
|
||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||
|
||||
let provider = match common::get_provider(udid, host, pairing_file, "afc-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut afc_client = AfcClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Unable to connect to misagent");
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("list") {
|
||||
let path = matches.get_one::<String>("path").expect("No path passed");
|
||||
let res = afc_client.list(path).await.expect("Failed to read dir");
|
||||
println!("{path}\n{res:#?}");
|
||||
} else if let Some(matches) = matches.subcommand_matches("remove") {
|
||||
let path = matches.get_one::<String>("id").expect("No path passed");
|
||||
} else {
|
||||
eprintln!("Invalid usage, pass -h for help");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user