diff --git a/ffi/src/errors.rs b/ffi/src/errors.rs new file mode 100644 index 0000000..e4d37db --- /dev/null +++ b/ffi/src/errors.rs @@ -0,0 +1,96 @@ +// Jackson Coxson + +use idevice::IdeviceError; + +#[repr(C)] +pub enum IdeviceErrorCode { + IdeviceSuccess = 0, + // Main library + Socket = -1, + Ssl = -2, + SslSetup = -3, + Plist = -4, + Utf8 = -5, + UnexpectedResponse = -6, + GetProhibited = -7, + SessionInactive = -8, + InvalidHostID = -9, + NoEstablishedConnection = -10, + HeartbeatSleepyTime = -11, + HeartbeatTimeout = -12, + NotFound = -13, + CdtunnelPacketTooShort = -14, + CdtunnelPacketInvalidMagic = -15, + PacketSizeMismatch = -16, + Json = -17, + DeviceNotFound = -18, + DeviceLocked = -19, + UsbConnectionRefused = -20, + UsbBadCommand = -21, + UsbBadDevice = -22, + UsbBadVersion = -23, + BadBuildManifest = -24, + ImageNotMounted = -25, + Reqwest = -26, + InternalError = -27, + Xpc = -28, + NsKeyedArchiveError = -29, + UnknownAuxValueType = -30, + UnknownChannel = -31, + AddrParseError = -32, + DisableMemoryLimitFailed = -33, + NotEnoughBytes = -34, + Utf8Error = -35, + InvalidArgument = -36, + UnknownErrorType = -37, + // FFI specific bindings + InvalidString = -999, + InvalidArg = -1000, +} + +impl From for IdeviceErrorCode { + fn from(err: IdeviceError) -> Self { + match err { + IdeviceError::Socket(_) => IdeviceErrorCode::Socket, + IdeviceError::Ssl(_) => IdeviceErrorCode::Ssl, + IdeviceError::SslSetup(_) => IdeviceErrorCode::SslSetup, + IdeviceError::Plist(_) => IdeviceErrorCode::Plist, + IdeviceError::Utf8(_) => IdeviceErrorCode::Utf8, + IdeviceError::UnexpectedResponse => IdeviceErrorCode::UnexpectedResponse, + IdeviceError::GetProhibited => IdeviceErrorCode::GetProhibited, + IdeviceError::SessionInactive => IdeviceErrorCode::SessionInactive, + IdeviceError::InvalidHostID => IdeviceErrorCode::InvalidHostID, + IdeviceError::NoEstablishedConnection => IdeviceErrorCode::NoEstablishedConnection, + IdeviceError::HeartbeatSleepyTime => IdeviceErrorCode::HeartbeatSleepyTime, + IdeviceError::HeartbeatTimeout => IdeviceErrorCode::HeartbeatTimeout, + IdeviceError::NotFound => IdeviceErrorCode::NotFound, + IdeviceError::CdtunnelPacketTooShort => IdeviceErrorCode::CdtunnelPacketTooShort, + IdeviceError::CdtunnelPacketInvalidMagic => { + IdeviceErrorCode::CdtunnelPacketInvalidMagic + } + IdeviceError::PacketSizeMismatch => IdeviceErrorCode::PacketSizeMismatch, + IdeviceError::Json(_) => IdeviceErrorCode::Json, + IdeviceError::DeviceNotFound => IdeviceErrorCode::DeviceNotFound, + IdeviceError::DeviceLocked => IdeviceErrorCode::DeviceLocked, + IdeviceError::UsbConnectionRefused => IdeviceErrorCode::UsbConnectionRefused, + IdeviceError::UsbBadCommand => IdeviceErrorCode::UsbBadCommand, + IdeviceError::UsbBadDevice => IdeviceErrorCode::UsbBadDevice, + IdeviceError::UsbBadVersion => IdeviceErrorCode::UsbBadVersion, + IdeviceError::BadBuildManifest => IdeviceErrorCode::BadBuildManifest, + IdeviceError::ImageNotMounted => IdeviceErrorCode::ImageNotMounted, + IdeviceError::Reqwest(_) => IdeviceErrorCode::Reqwest, + IdeviceError::InternalError(_) => IdeviceErrorCode::InternalError, + IdeviceError::Xpc(_) => IdeviceErrorCode::Xpc, + IdeviceError::NsKeyedArchiveError(_) => IdeviceErrorCode::NsKeyedArchiveError, + IdeviceError::UnknownAuxValueType(_) => IdeviceErrorCode::UnknownAuxValueType, + IdeviceError::UnknownChannel(_) => IdeviceErrorCode::UnknownChannel, + IdeviceError::AddrParseError(_) => IdeviceErrorCode::AddrParseError, + IdeviceError::DisableMemoryLimitFailed => IdeviceErrorCode::DisableMemoryLimitFailed, + IdeviceError::NotEnoughBytes(_, _) => IdeviceErrorCode::NotEnoughBytes, + IdeviceError::Utf8Error => IdeviceErrorCode::Utf8Error, + IdeviceError::InvalidArgument => IdeviceErrorCode::InvalidArgument, + IdeviceError::UnknownErrorType(_) => IdeviceErrorCode::UnknownErrorType, + _ => IdeviceErrorCode::InternalError, + } + } +} diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs new file mode 100644 index 0000000..09f58da --- /dev/null +++ b/ffi/src/lib.rs @@ -0,0 +1,261 @@ +// Jackson Coxson + +mod errors; +pub mod logging; +mod pairing_file; +pub mod provider; +pub mod usbmuxd; +pub mod util; + +pub use errors::*; +pub use pairing_file::*; + +use idevice::{Idevice, IdeviceSocket}; +use once_cell::sync::Lazy; +use std::ffi::{CStr, CString, c_char}; +use tokio::runtime::{self, Runtime}; + +static RUNTIME: Lazy = Lazy::new(|| { + runtime::Builder::new_multi_thread() + .enable_io() + .build() + .unwrap() +}); + +pub const LOCKDOWN_PORT: u16 = 62078; + +/// Opaque C-compatible handle to an Idevice connection +pub struct IdeviceHandle(pub Idevice); +pub struct IdeviceSocketHandle(IdeviceSocket); + +// https://github.com/mozilla/cbindgen/issues/539 +#[allow(non_camel_case_types, unused)] +struct sockaddr; + +/// Creates a new Idevice connection +/// +/// # Arguments +/// * [`socket`] - Socket for communication with the device +/// * [`label`] - Label for the connection +/// * [`idevice`] - On success, will be set to point to a newly allocated Idevice handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `label` must be a valid null-terminated C string +/// `idevice` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_new( + socket: *mut IdeviceSocketHandle, + label: *const c_char, + idevice: *mut *mut IdeviceHandle, +) -> IdeviceErrorCode { + if socket.is_null() || label.is_null() || idevice.is_null() { + return IdeviceErrorCode::InvalidArg; + } + + // Get socket ownership + let socket_box = unsafe { Box::from_raw(socket) }; + + // Convert C string to Rust string + let c_str = match unsafe { CStr::from_ptr(label).to_str() } { + Ok(s) => s, + Err(_) => return IdeviceErrorCode::InvalidString, + }; + + // Create new Idevice instance + let dev = Idevice::new((*socket_box).0, c_str); + let boxed = Box::new(IdeviceHandle(dev)); + unsafe { *idevice = Box::into_raw(boxed) }; + + IdeviceErrorCode::IdeviceSuccess +} + +/// Creates a new Idevice connection +/// +/// # Arguments +/// * [`addr`] - The socket address to connect to +/// * [`addr_len`] - Length of the socket +/// * [`label`] - Label for the connection +/// * [`idevice`] - On success, will be set to point to a newly allocated Idevice handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `addr` must be a valid sockaddr +/// `label` must be a valid null-terminated C string +/// `idevice` must be a valid, non-null pointer to a location where the handle will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_new_tcp_socket( + addr: *const libc::sockaddr, + addr_len: libc::socklen_t, + label: *const c_char, + idevice: *mut *mut IdeviceHandle, +) -> IdeviceErrorCode { + if addr.is_null() { + log::error!("socket addr null pointer"); + return IdeviceErrorCode::InvalidArg; + } + + // Convert C string to Rust string + let label = match unsafe { CStr::from_ptr(label).to_str() } { + Ok(s) => s, + Err(_) => return IdeviceErrorCode::InvalidArg, + }; + + let addr = match util::c_socket_to_rust(addr, addr_len) { + Ok(a) => a, + Err(e) => return e, + }; + + let device: Result = RUNTIME.block_on(async move { + Ok(idevice::Idevice::new( + Box::new(tokio::net::TcpStream::connect(addr).await?), + label, + )) + }); + + match device { + Ok(dev) => { + let boxed = Box::new(IdeviceHandle(dev)); + unsafe { *idevice = Box::into_raw(boxed) }; + IdeviceErrorCode::IdeviceSuccess + } + Err(e) => e.into(), + } +} + +/// Gets the device type +/// +/// # Arguments +/// * [`idevice`] - The Idevice handle +/// * [`device_type`] - On success, will be set to point to a newly allocated string containing the device type +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `idevice` must be a valid, non-null pointer to an Idevice handle +/// `device_type` must be a valid, non-null pointer to a location where the string pointer will be stored +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_get_type( + idevice: *mut IdeviceHandle, + device_type: *mut *mut c_char, +) -> IdeviceErrorCode { + if idevice.is_null() || device_type.is_null() { + return IdeviceErrorCode::InvalidArg; + } + + // Get the Idevice reference + let dev = unsafe { &mut (*idevice).0 }; + + // Run the get_type method in the runtime + let result = RUNTIME.block_on(async { dev.get_type().await }); + + match result { + Ok(type_str) => match CString::new(type_str) { + Ok(c_string) => { + unsafe { *device_type = c_string.into_raw() }; + IdeviceErrorCode::IdeviceSuccess + } + Err(_) => IdeviceErrorCode::InvalidString, + }, + Err(e) => e.into(), + } +} + +/// Performs RSD checkin +/// +/// # Arguments +/// * [`idevice`] - The Idevice handle +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `idevice` must be a valid, non-null pointer to an Idevice handle +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_rsd_checkin(idevice: *mut IdeviceHandle) -> IdeviceErrorCode { + if idevice.is_null() { + return IdeviceErrorCode::InvalidArg; + } + + // Get the Idevice reference + let dev = unsafe { &mut (*idevice).0 }; + + // Run the rsd_checkin method in the runtime + let result = RUNTIME.block_on(async { dev.rsd_checkin().await }); + + match result { + Ok(_) => IdeviceErrorCode::IdeviceSuccess, + Err(e) => e.into(), + } +} + +/// Starts a TLS session +/// +/// # Arguments +/// * [`idevice`] - The Idevice handle +/// * [`pairing_file`] - The pairing file to use for TLS +/// +/// # Returns +/// An error code indicating success or failure +/// +/// # Safety +/// `idevice` must be a valid, non-null pointer to an Idevice handle +/// `pairing_file` must be a valid, non-null pointer to a pairing file handle +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_start_session( + idevice: *mut IdeviceHandle, + pairing_file: *const IdevicePairingFile, +) -> IdeviceErrorCode { + if idevice.is_null() || pairing_file.is_null() { + return IdeviceErrorCode::InvalidArg; + } + + // Get the Idevice reference + let dev = unsafe { &mut (*idevice).0 }; + + // Get the pairing file reference + let pf = unsafe { &(*pairing_file).0 }; + + // Run the start_session method in the runtime + let result = RUNTIME.block_on(async { dev.start_session(pf).await }); + + match result { + Ok(_) => IdeviceErrorCode::IdeviceSuccess, + Err(e) => e.into(), + } +} + +/// Frees an Idevice handle +/// +/// # Arguments +/// * [`idevice`] - The Idevice handle to free +/// +/// # Safety +/// `idevice` must be a valid pointer to an Idevice handle that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free(idevice: *mut IdeviceHandle) { + if !idevice.is_null() { + let _ = unsafe { Box::from_raw(idevice) }; + } +} + +/// Frees a string allocated by this library +/// +/// # Arguments +/// * [`string`] - The string to free +/// +/// # Safety +/// `string` must be a valid pointer to a string that was allocated by this library, +/// or NULL (in which case this function does nothing) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_string_free(string: *mut c_char) { + if !string.is_null() { + let _ = unsafe { CString::from_raw(string) }; + } +} diff --git a/ffi/src/logging.rs b/ffi/src/logging.rs new file mode 100644 index 0000000..404d873 --- /dev/null +++ b/ffi/src/logging.rs @@ -0,0 +1,124 @@ +// Jackson Coxson + +use std::{ + ffi::{CString, c_char}, + fs::File, +}; + +use log::LevelFilter; +use simplelog::{ + ColorChoice, CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode, WriteLogger, +}; + +/// Initializes the logger +/// +/// # Arguments +/// * [`console_level`] - The level to log to the file +/// * [`file_level`] - The level to log to the file +/// * [`file_path`] - If not null, the file to write logs to +/// +/// ## Log Level +/// 0. Disabled +/// 1. Error +/// 2. Warn +/// 3. Info +/// 4. Debug +/// 5. Trace +/// +/// # Returns +/// 0 for success, -1 if the file couldn't be created, -2 if a logger has been initialized, -3 for invalid path string +/// +/// # Safety +/// Pass a valid CString for file_path. Pass valid log levels according to the enum +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_init_logger( + console_level: IdeviceLogLevel, + file_level: IdeviceLogLevel, + file_path: *mut c_char, +) -> IdeviceLoggerError { + let mut loggers: Vec> = Vec::new(); + let level: LevelFilter = console_level.into(); + loggers.push(TermLogger::new( + level, + Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + )); + + if !file_path.is_null() { + let file_path = match unsafe { CString::from_raw(file_path) }.to_str() { + Ok(f) => f.to_string(), + Err(_) => { + return IdeviceLoggerError::InvalidPathString; + } + }; + let level: LevelFilter = file_level.into(); + loggers.push(WriteLogger::new( + level, + Config::default(), + match File::create(file_path) { + Ok(f) => f, + Err(e) => { + println!("Failed to create path: {e:?}"); + return IdeviceLoggerError::FileError; + } + }, + )); + } + + if CombinedLogger::init(loggers).is_err() { + IdeviceLoggerError::AlreadyInitialized + } else { + IdeviceLoggerError::Success + } +} + +#[repr(C)] +pub enum IdeviceLoggerError { + Success = 0, + FileError = -1, + AlreadyInitialized = -2, + InvalidPathString = -3, +} + +#[repr(C)] +#[derive(PartialEq)] +pub enum IdeviceLogLevel { + Disabled = 0, + ErrorLevel = 1, + Warn = 2, + Info = 3, + Debug = 4, + Trace = 5, +} + +impl TryFrom for IdeviceLogLevel { + type Error = (); + + fn try_from(value: u8) -> Result { + Ok(match value { + 0 => Self::Disabled, + 1 => Self::ErrorLevel, + 2 => Self::Warn, + 3 => Self::Info, + 4 => Self::Debug, + 5 => Self::Trace, + _ => { + return Err(()); + } + }) + } +} + +impl From for LevelFilter { + fn from(value: IdeviceLogLevel) -> Self { + match value { + IdeviceLogLevel::Disabled => LevelFilter::Off, + IdeviceLogLevel::ErrorLevel => LevelFilter::Error, + IdeviceLogLevel::Warn => LevelFilter::Warn, + IdeviceLogLevel::Info => LevelFilter::Info, + IdeviceLogLevel::Debug => LevelFilter::Debug, + IdeviceLogLevel::Trace => LevelFilter::Trace, + } + } +}