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 = []
|
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
|
||||||
|
|||||||
@@ -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
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> {
|
pub fn as_string(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
XPCObject::String(s) => Some(s),
|
XPCObject::String(s) => Some(s),
|
||||||
|
|||||||
@@ -1,33 +1,144 @@
|
|||||||
// Thanks DebianArch
|
// Thanks DebianArch
|
||||||
|
|
||||||
use crate::http2::{
|
use std::collections::HashMap;
|
||||||
self,
|
|
||||||
h2::{SettingsFrame, WindowUpdateFrame},
|
use crate::{
|
||||||
|
http2::{
|
||||||
|
self,
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
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