Implement getting services from RemoteXPC

This commit is contained in:
Jackson Coxson
2025-02-25 20:57:06 -07:00
parent 75cd32c166
commit 44142dbdbe
7 changed files with 307 additions and 31 deletions

View File

@@ -43,6 +43,7 @@ mounter = ["dep:sha2"]
usbmuxd = [] usbmuxd = []
tcp = ["tokio/net"] tcp = ["tokio/net"]
tss = ["dep:uuid", "dep:reqwest"] tss = ["dep:uuid", "dep:reqwest"]
tunneld = ["dep:serde_json", "dep:json", "dep:reqwest"]
xpc = [ xpc = [
"tokio/sync", "tokio/sync",
"dep:indexmap", "dep:indexmap",
@@ -59,6 +60,7 @@ full = [
"xpc", "xpc",
"tcp", "tcp",
"tss", "tss",
"tunneld",
] ]
# Why: https://github.com/rust-lang/cargo/issues/1197 # Why: https://github.com/rust-lang/cargo/issues/1197

View File

@@ -15,6 +15,8 @@ pub mod pairing_file;
pub mod provider; pub mod provider;
#[cfg(feature = "tss")] #[cfg(feature = "tss")]
pub mod tss; pub mod tss;
#[cfg(feature = "tunneld")]
pub mod tunneld;
#[cfg(feature = "usbmuxd")] #[cfg(feature = "usbmuxd")]
pub mod usbmuxd; pub mod usbmuxd;
mod util; 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 /// Sends a plist to the socket
async fn send_plist(&mut self, message: plist::Value) -> Result<(), IdeviceError> { async fn send_plist(&mut self, message: plist::Value) -> Result<(), IdeviceError> {
if let Some(socket) = &mut self.socket { if let Some(socket) = &mut self.socket {
@@ -259,6 +290,10 @@ pub enum IdeviceError {
#[error("internal error")] #[error("internal error")]
InternalError(String), InternalError(String),
#[cfg(feature = "xpc")]
#[error("xpc message failed")]
Xpc(#[from] xpc::error::XPCError),
#[error("unknown error `{0}` returned from device")] #[error("unknown error `{0}` returned from device")]
UnknownErrorType(String), UnknownErrorType(String),
} }

70
idevice/src/tunneld.rs Normal file
View File

@@ -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<HashMap<String, TunneldDevice>, 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<TunneldDevice> = 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);
}
}

View File

@@ -326,6 +326,13 @@ impl XPCObject {
} }
} }
pub fn as_array(&self) -> Option<&Vec<Self>> {
match self {
XPCObject::Array(array) => Some(array),
_ => None,
}
}
pub fn as_string(&self) -> Option<&str> { pub fn as_string(&self) -> Option<&str> {
match self { match self {
XPCObject::String(s) => Some(s), XPCObject::String(s) => Some(s),

View File

@@ -1,33 +1,144 @@
// Thanks DebianArch // Thanks DebianArch
use crate::http2::{ use std::collections::HashMap;
use crate::{
http2::{
self, self,
h2::{SettingsFrame, WindowUpdateFrame}, h2::{SettingsFrame, WindowUpdateFrame},
},
IdeviceError,
}; };
use error::XPCError; use error::XPCError;
use format::{XPCFlag, XPCMessage, XPCObject}; use format::{XPCFlag, XPCMessage, XPCObject};
use log::debug; use log::{debug, warn};
use tokio::net::{TcpStream, ToSocketAddrs}; use serde::Deserialize;
pub mod cdtunnel; pub mod cdtunnel;
pub mod error; pub mod error;
pub mod format; pub mod format;
pub struct XPCDevice {
pub connection: XPCConnection,
pub services: HashMap<String, XPCService>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct XPCService {
pub entitlement: String,
pub port: u16,
pub uses_remote_xpc: bool,
pub features: Option<Vec<String>>,
pub service_version: Option<i64>,
}
pub struct XPCConnection { pub struct XPCConnection {
inner: http2::Connection, inner: http2::Connection,
root_message_id: u64, root_message_id: u64,
reply_message_id: u64, reply_message_id: u64,
} }
impl XPCDevice {
pub async fn new(stream: crate::IdeviceSocket) -> Result<Self, IdeviceError> {
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::<u16>().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::<Vec<String>>()
});
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 { impl XPCConnection {
pub const ROOT_CHANNEL: u32 = http2::Connection::ROOT_CHANNEL; pub const ROOT_CHANNEL: u32 = http2::Connection::ROOT_CHANNEL;
pub const REPLY_CHANNEL: u32 = http2::Connection::REPLY_CHANNEL; pub const REPLY_CHANNEL: u32 = http2::Connection::REPLY_CHANNEL;
const INIT_STREAM: u32 = http2::Connection::INIT_STREAM; const INIT_STREAM: u32 = http2::Connection::INIT_STREAM;
pub async fn connect<A: ToSocketAddrs>(addr: A) -> Result<Self, XPCError> {
Self::new(Box::new(TcpStream::connect(addr).await?)).await
}
pub async fn new(stream: crate::IdeviceSocket) -> Result<Self, XPCError> { pub async fn new(stream: crate::IdeviceSocket) -> Result<Self, XPCError> {
let mut client = http2::Connection::new(stream).await?; let mut client = http2::Connection::new(stream).await?;
client 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);
}
}

View File

@@ -33,6 +33,10 @@ path = "src/core_device_proxy_tun.rs"
name = "idevice_id" name = "idevice_id"
path = "src/idevice_id.rs" path = "src/idevice_id.rs"
[[bin]]
name = "remotexpc"
path = "src/remotexpc.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"] } idevice = { path = "../idevice", features = ["full"] }

69
tools/src/remotexpc.rs Normal file
View File

@@ -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::<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();
println!("{:#?}", client.services);
}