From 44142dbdbe61202a4ed68843bbdfbf83123dcc7b Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Tue, 25 Feb 2025 20:57:06 -0700 Subject: [PATCH] Implement getting services from RemoteXPC --- idevice/Cargo.toml | 2 + idevice/src/lib.rs | 35 +++++++++ idevice/src/tunneld.rs | 70 ++++++++++++++++++ idevice/src/xpc/format.rs | 7 ++ idevice/src/xpc/mod.rs | 151 ++++++++++++++++++++++++++++++-------- tools/Cargo.toml | 4 + tools/src/remotexpc.rs | 69 +++++++++++++++++ 7 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 idevice/src/tunneld.rs create mode 100644 tools/src/remotexpc.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index ade1cb7..3ea3534 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -43,6 +43,7 @@ mounter = ["dep:sha2"] usbmuxd = [] tcp = ["tokio/net"] tss = ["dep:uuid", "dep:reqwest"] +tunneld = ["dep:serde_json", "dep:json", "dep:reqwest"] xpc = [ "tokio/sync", "dep:indexmap", @@ -59,6 +60,7 @@ full = [ "xpc", "tcp", "tss", + "tunneld", ] # Why: https://github.com/rust-lang/cargo/issues/1197 diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 6f8773d..9c2acb1 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -15,6 +15,8 @@ pub mod pairing_file; pub mod provider; #[cfg(feature = "tss")] pub mod tss; +#[cfg(feature = "tunneld")] +pub mod tunneld; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; mod util; @@ -68,6 +70,35 @@ impl Idevice { } } + pub async fn rsd_checkin(&mut self) -> Result<(), IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("Label".into(), self.label.clone().into()); + req.insert("ProtocolVersion".into(), "2".into()); + req.insert("Request".into(), "RSDCheckin".into()); + self.send_plist(plist::to_value(&req).unwrap()).await?; + let res = self.read_plist().await?; + match res.get("Request").and_then(|x| x.as_string()) { + Some(r) => { + if r != "RSDCheckin" { + return Err(IdeviceError::UnexpectedResponse); + } + } + None => return Err(IdeviceError::UnexpectedResponse), + } + + let res = self.read_plist().await?; + match res.get("Request").and_then(|x| x.as_string()) { + Some(r) => { + if r != "StartService" { + return Err(IdeviceError::UnexpectedResponse); + } + } + None => return Err(IdeviceError::UnexpectedResponse), + } + + Ok(()) + } + /// Sends a plist to the socket async fn send_plist(&mut self, message: plist::Value) -> Result<(), IdeviceError> { if let Some(socket) = &mut self.socket { @@ -259,6 +290,10 @@ pub enum IdeviceError { #[error("internal error")] InternalError(String), + #[cfg(feature = "xpc")] + #[error("xpc message failed")] + Xpc(#[from] xpc::error::XPCError), + #[error("unknown error `{0}` returned from device")] UnknownErrorType(String), } diff --git a/idevice/src/tunneld.rs b/idevice/src/tunneld.rs new file mode 100644 index 0000000..9c94a8a --- /dev/null +++ b/idevice/src/tunneld.rs @@ -0,0 +1,70 @@ +// Shim code for using pymobiledevice3's tunneld + +use std::{collections::HashMap, net::SocketAddr}; + +use log::warn; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::IdeviceError; + +pub const DEFAULT_PORT: u16 = 49151; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TunneldDevice { + pub interface: String, + #[serde(rename = "tunnel-address")] + pub tunnel_address: String, + #[serde(rename = "tunnel-port")] + pub tunnel_port: u16, +} + +pub async fn get_tunneld_devices( + socket: SocketAddr, +) -> Result, IdeviceError> { + let res: Value = reqwest::get(format!("http://{socket}")) + .await? + .json() + .await?; + + let res = match res.as_object() { + Some(r) => r, + None => { + warn!("tunneld return type wasn't a dictionary"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let mut to_return = HashMap::new(); + for (udid, v) in res.into_iter() { + let mut v: Vec = match serde_json::from_value(v.clone()) { + Ok(v) => v, + Err(e) => { + warn!("Failed to parse tunneld results as vector of struct: {e:?}"); + continue; + } + }; + + if v.is_empty() { + warn!("Device had no entries"); + continue; + } + + to_return.insert(udid.clone(), v.remove(0)); + } + + Ok(to_return) +} + +#[cfg(test)] +mod tests { + use std::{net::IpAddr, str::FromStr}; + + use super::*; + + #[tokio::test] + async fn t1() { + let host = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), DEFAULT_PORT); + println!("{:#?}", get_tunneld_devices(host).await); + } +} diff --git a/idevice/src/xpc/format.rs b/idevice/src/xpc/format.rs index c183523..9e850e1 100644 --- a/idevice/src/xpc/format.rs +++ b/idevice/src/xpc/format.rs @@ -326,6 +326,13 @@ impl XPCObject { } } + pub fn as_array(&self) -> Option<&Vec> { + match self { + XPCObject::Array(array) => Some(array), + _ => None, + } + } + pub fn as_string(&self) -> Option<&str> { match self { XPCObject::String(s) => Some(s), diff --git a/idevice/src/xpc/mod.rs b/idevice/src/xpc/mod.rs index 177774c..be6e204 100644 --- a/idevice/src/xpc/mod.rs +++ b/idevice/src/xpc/mod.rs @@ -1,33 +1,144 @@ // Thanks DebianArch -use crate::http2::{ - self, - h2::{SettingsFrame, WindowUpdateFrame}, +use std::collections::HashMap; + +use crate::{ + http2::{ + self, + h2::{SettingsFrame, WindowUpdateFrame}, + }, + IdeviceError, }; use error::XPCError; use format::{XPCFlag, XPCMessage, XPCObject}; -use log::debug; -use tokio::net::{TcpStream, ToSocketAddrs}; +use log::{debug, warn}; +use serde::Deserialize; pub mod cdtunnel; pub mod error; pub mod format; +pub struct XPCDevice { + pub connection: XPCConnection, + pub services: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct XPCService { + pub entitlement: String, + pub port: u16, + pub uses_remote_xpc: bool, + pub features: Option>, + pub service_version: Option, +} + pub struct XPCConnection { inner: http2::Connection, root_message_id: u64, reply_message_id: u64, } +impl XPCDevice { + pub async fn new(stream: crate::IdeviceSocket) -> Result { + let mut connection = XPCConnection::new(stream).await?; + + let data = connection + .read_message(http2::Connection::ROOT_CHANNEL) + .await?; + + let data = match data.message { + Some(d) => match d + .as_dictionary() + .and_then(|x| x.get("Services")) + .and_then(|x| x.as_dictionary()) + { + Some(d) => d.to_owned(), + None => return Err(IdeviceError::UnexpectedResponse), + }, + None => return Err(IdeviceError::UnexpectedResponse), + }; + + let mut services = HashMap::new(); + for (name, service) in data.into_iter() { + match service.as_dictionary() { + Some(service) => { + let entitlement = match service.get("Entitlement").and_then(|x| x.as_string()) { + Some(e) => e.to_string(), + None => { + warn!("Service did not contain entitlement string"); + continue; + } + }; + let port = match service + .get("Port") + .and_then(|x| x.as_string()) + .and_then(|x| x.parse::().ok()) + { + Some(e) => e, + None => { + warn!("Service did not contain port string"); + continue; + } + }; + let uses_remote_xpc = match service + .get("Properties") + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("UsesRemoteXPC")) + .and_then(|x| x.as_bool()) + { + Some(e) => e.to_owned(), + None => false, // default is false + }; + + let features = service + .get("Properties") + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("Features")) + .and_then(|x| x.as_array()) + .map(|f| { + f.iter() + .filter_map(|x| x.as_string()) + .map(|x| x.to_string()) + .collect::>() + }); + + let service_version = service + .get("Properties") + .and_then(|x| x.as_dictionary()) + .and_then(|x| x.get("ServiceVersion")) + .and_then(|x| x.as_signed_integer()) + .map(|e| e.to_owned()); + + services.insert( + name, + XPCService { + entitlement, + port, + uses_remote_xpc, + features, + service_version, + }, + ); + } + None => { + warn!("Service is not a dictionary!"); + continue; + } + } + } + + Ok(Self { + connection, + services, + }) + } +} + impl XPCConnection { pub const ROOT_CHANNEL: u32 = http2::Connection::ROOT_CHANNEL; pub const REPLY_CHANNEL: u32 = http2::Connection::REPLY_CHANNEL; const INIT_STREAM: u32 = http2::Connection::INIT_STREAM; - pub async fn connect(addr: A) -> Result { - Self::new(Box::new(TcpStream::connect(addr).await?)).await - } - pub async fn new(stream: crate::IdeviceSocket) -> Result { let mut client = http2::Connection::new(stream).await?; client @@ -123,25 +234,3 @@ impl XPCConnection { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn it_works() { - let mut client = XPCConnection::new(Box::new( - TcpStream::connect(("fdca:2653:ece9::1", 64497)) - .await - .unwrap(), - )) - .await - .unwrap(); - - let data = client - .read_message(http2::Connection::ROOT_CHANNEL) - .await - .unwrap(); - println!("{:#?}", data); - } -} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 1e3cd10..b2950b5 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -33,6 +33,10 @@ path = "src/core_device_proxy_tun.rs" name = "idevice_id" path = "src/idevice_id.rs" +[[bin]] +name = "remotexpc" +path = "src/remotexpc.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } diff --git a/tools/src/remotexpc.rs b/tools/src/remotexpc.rs new file mode 100644 index 0000000..4526205 --- /dev/null +++ b/tools/src/remotexpc.rs @@ -0,0 +1,69 @@ +// Jackson Coxson +// Print out all the RemoteXPC services + +use std::{ + net::{IpAddr, SocketAddr}, + str::FromStr, +}; + +use clap::{Arg, Command}; +use idevice::{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!("remotexpc - get info from RemoteXPC"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let udid = matches.get_one::("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(); + + println!("{:#?}", client.services); +}