mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Implement getting services from RemoteXPC
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
70
idevice/src/tunneld.rs
Normal file
70
idevice/src/tunneld.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
match self {
|
||||
XPCObject::String(s) => Some(s),
|
||||
|
||||
@@ -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<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 {
|
||||
inner: http2::Connection,
|
||||
root_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 {
|
||||
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<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> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
69
tools/src/remotexpc.rs
Normal file
69
tools/src/remotexpc.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user