Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 220 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

43 changes: 36 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PlumeImpactor
# <img src="https://github.com/user-attachments/assets/18f2eff4-546f-4365-98eb-afb19b13dc13" width="25" height="25" /> Impactor

[![GitHub Release](https://img.shields.io/github/v/release/khcrysalis/PlumeImpactor?include_prereleases)](https://github.com/khcrysalis/PlumeImpactor/releases)
[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/khcrysalis/PlumeImpactor/total)](https://github.com/khcrysalis/PlumeImpactor/releases)
Expand All @@ -11,24 +11,25 @@ Open-source, cross-platform, and feature rich iOS sideloading application. Suppo

[^2]: On Windows, [iTunes](https://support.apple.com/en-us/106372) must be downloaded so Impactor is able to use the drivers for interacting with Apple devices.

| ![Demo of app](demo.png) |
| :--------------------------------------------------------------------------------------------------: |
| Screenshot of Impactor after importing [Feather](https://github.com/khcrysalis/Feather). |
![Demo of app](demo.png)

### Features

- User friendly and clean UI.
- Supports Linux.
- Sign and sideload applications on iOS 9.0+ & Mac with your Apple ID.
- Installing with AppSync is supported.
- Installing with [AppSync](https://github.com/akemin-dayo/AppSync) is supported.
- Installing with ipatool gotten ipa's is supported.
- Automatically disables updates from the App Store.
- Simple customization options for the app.
- Tweak support for advanced users, using [ElleKit](https://github.com/tealbathingsuit/ellekit) for injection.
- Supports injecting `.deb` and `.dylib` files.
- Supports adding `.framework`, `.bundle`, and `.appex` directories.
- Generates P12 for SideStore/AltStore to use, similar to how Altserver works.
- Supports replacing Cydia Substrate with ElleKit for 26.0 compatibility.
- Generates P12 for SideStore/AltStore to use, similar to Altserver.
- Automatically populate pairing files for apps like SideStore, Antrag, and Protokolle.
- Comes with simple device utilities for retrusting/placing pairing file.
- Export P12 for use with LiveContainer.
- Almost *proper* entitlement handling and can register app plugins.
- Able to request entitlements like `increased-memory-limit`, for emulators like MelonX or UTM.

Expand All @@ -50,6 +51,21 @@ Lastly, we do all of the necessary modifications we need to the app you're tryin

That's the entire gist of how this works! Of course its very short and brief, however feel free to look how it works since its open source :D

### Pairing File

Impactor also allows the user to generate a pairing file for applications to talk directly to the device remotely. This pairing file is device specific and will become invalid if you ever re-trust/update/reset.

Supported apps:
- `SideStore`
- `Feather`
- `SparseBox`
- `LiveContainer + SideStore`
- `Antrag`
- `Protokolle`
- `StikDebug`

You can retrieve this file by either sideloading the supported app of your choice, or going to the `Utilities` page when a device is connected and press install for the supported app. Head over to the [downloads](https://github.com/khcrysalis/PlumeImpactor/releases).

## Structure

The project is seperated in multiple modules, all serve single or multiple uses depending on their importance.
Expand All @@ -67,7 +83,6 @@ The project is seperated in multiple modules, all serve single or multiple uses

Building is going to be a bit convoluted for each platform, each having their own unique specifications, but the best reference for building should be looking at how [GitHub actions](./.github/workflows/build.yml) does it.


You need:
- [Rust](https://rustup.rs/).
- [CMake](https://cmake.org/download/) (and a c++ compiler).
Expand Down Expand Up @@ -107,6 +122,16 @@ sudo dnf install clang-devel pkg-config gtk3-devel libpng-devel libjpeg-devel me
| <img src="https://raw.githubusercontent.com/khcrysalis/github-sponsor-graph/main/graph.png"> |
| _**"samara is cute" - Vendicated**_ |

## Star History

<a href="https://star-history.com/#khcrysalis/plumeimpactor&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=khcrysalis/plumeimpactor&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=khcrysalis/plumeimpactor&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=khcrysalis/plumeimpactor&type=Date" />
</picture>
</a>

## Acknowledgements

- [SAMSAM](https://github.com/khcrysalis) – The maker.
Expand All @@ -117,6 +142,10 @@ sudo dnf install clang-devel pkg-config gtk3-devel libpng-devel libjpeg-devel me
- [idevice](https://github.com/jkcoxson/idevice) – Used for communication with `installd`, specifically for sideloading the apps to your devices.
- [apple-codesign-rs](https://github.com/indygreg/apple-platform-rs) – Codesign alternative, modified and extended upon to work for Impactor.

<a href="https://github.com/iced-rs/iced">
<img src="https://gist.githubusercontent.com/hecrj/ad7ecd38f6e47ff3688a38c79fd108f0/raw/74384875ecbad02ae2a926425e9bcafd0695bade/color.svg" width="130px">
</a>

## License

Project is licensed under the MIT license. You can see the full details of the license [here](https://github.com/khcrysalis/PlumeImpactor/blob/main/LICENSE). Some components may be licensed under different licenses, see their respective directories for details.
7 changes: 5 additions & 2 deletions apps/plumeimpactor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ tray-icon = { version = "0.21.2", default-features = false }
image = "0.25"
rfd = "0.16.0"
open = "5.3.3"
notify-rust = "4.11.7"
single-instance = "0.3.3"
auto-launcher = "0.6.1"

[target.'cfg(target_os = "macos")'.dependencies]
plume_gestalt = { path = "../../crates/plume_gestalt" }

[target.'cfg(target_os = "windows")'.build-dependencies]
winres = "0.1"
[build-dependencies]
winres = { git = "https://github.com/Nilstrieb/winres", branch = "linking-flags" }

[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
21 changes: 16 additions & 5 deletions apps/plumeimpactor/build.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
fn main() {
#[cfg(windows)]
{
let mut res = winres::WindowsResource::new();
use std::io;
use winres::WindowsResource;

fn main() -> io::Result<()> {
if std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "windows" {
let mut res = WindowsResource::new();
match std::env::var("CARGO_CFG_TARGET_ENV").unwrap().as_str() {
"gnu" => {
res.set_ar_path("x86_64-w64-mingw32-ar")
.set_windres_path("x86_64-w64-mingw32-windres");
}
"msvc" => {}
_ => panic!("unsupported env"),
};
res.set_icon("../../package/windows/icon.ico");
res.compile().unwrap();
res.compile()?;
}
Ok(())
}
25 changes: 25 additions & 0 deletions apps/plumeimpactor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

use crate::refresh::spawn_refresh_daemon;

#[cfg(any(target_os = "linux", target_os = "windows"))]
use single_instance::SingleInstance;

mod appearance;
mod defaults;
mod refresh;
mod screen;
mod subscriptions;
mod startup;
mod tray;

pub const APP_NAME: &str = "Impactor";
Expand All @@ -16,11 +20,32 @@ fn main() -> iced::Result {
env_logger::init();
let _ = rustls::crypto::ring::default_provider().install_default();

#[cfg(any(target_os = "linux", target_os = "windows"))]
let _single_instance = match SingleInstance::new(APP_NAME) {
Ok(instance) => {
if !instance.is_single() {
log::info!("Another instance is already running; exiting.");
return Ok(());
}
Some(instance)
}
Err(err) => {
log::warn!("Failed to acquire single-instance lock: {err}");
None
}
};

#[cfg(target_os = "linux")]
{
gtk::init().expect("GTK init failed");
}

#[cfg(target_os = "macos")]
{
notify_rust::get_bundle_identifier_or_default("Impactor");
let _ = notify_rust::set_application("dev.khcrysalis.PlumeImpactor");
}

let (_daemon_handle, connected_devices) = spawn_refresh_daemon();
screen::set_refresh_daemon_devices(connected_devices);

Expand Down
127 changes: 109 additions & 18 deletions apps/plumeimpactor/src/refresh.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
Expand All @@ -14,9 +14,24 @@ use crate::defaults::get_data_path;

pub type ConnectedDevices = Arc<Mutex<HashMap<String, Device>>>;

struct RefreshGuard {
udid: String,
tasks: Arc<Mutex<HashSet<String>>>,
}

impl Drop for RefreshGuard {
fn drop(&mut self) {
if let Ok(mut tasks) = self.tasks.lock() {
tasks.remove(&self.udid);
log::debug!("Released lock for device {}", self.udid);
}
}
}

pub struct RefreshDaemon {
store_path: std::path::PathBuf,
connected_devices: ConnectedDevices,
active_tasks: Arc<Mutex<HashSet<String>>>,
check_interval: Duration,
}

Expand All @@ -25,6 +40,7 @@ impl RefreshDaemon {
Self {
store_path: get_data_path().join("accounts.json"),
connected_devices: Arc::new(Mutex::new(HashMap::new())),
active_tasks: Arc::new(Mutex::new(HashSet::new())),
check_interval: Duration::from_secs(60 * 30), // Check every 30 minutes
}
}
Expand All @@ -43,6 +59,11 @@ impl RefreshDaemon {
loop {
if let Err(e) = rt.block_on(self.check_and_refresh()) {
log::error!("Refresh daemon error: {}", e);
notify_rust::Notification::new()
.summary("Impactor")
.body(&format!("Failed to refresh: {}", e))
.show()
.ok();
}

thread::sleep(self.check_interval);
Expand All @@ -60,29 +81,42 @@ impl RefreshDaemon {
for (udid, refresh_device) in store.refreshes() {
for app in &refresh_device.apps {
if app.scheduled_refresh <= now {
// We check for active tasks here to prevent the background loop
// from even starting a wait if a manual refresh is already running.
if self.is_busy(udid) {
log::info!(
"Device {} is already being processed. Skipping this app for now.",
udid
);
continue;
}

log::info!("App at {:?} needs refresh for device {}", app.path, udid);

// Note: wait_for_device might take a long time.
// refresh_app will double-check the lock once the device is found.
let device = self.wait_for_device(udid).await?;

self.refresh_app(&store, refresh_device, app, &device)
.await?;
if let Err(e) = self.refresh_app(&store, refresh_device, app, &device).await {
log::error!("Error refreshing app: {}", e);
}
}
}
}

Ok(())
}

fn is_busy(&self, udid: &str) -> bool {
self.active_tasks
.lock()
.map(|t| t.contains(udid))
.unwrap_or(false)
}

async fn wait_for_device(&self, udid: &str) -> Result<Device, String> {
log::info!("Waiting for device {} to connect...", udid);

if let Ok(devices) = self.connected_devices.lock() {
if let Some(device) = devices.get(udid) {
log::info!("Device {} is already connected", udid);
return Ok(device.clone());
}
}

let timeout = Duration::from_secs(60 * 60); // 1 hour timeout
let start = std::time::Instant::now();

Expand All @@ -109,8 +143,40 @@ impl RefreshDaemon {
app: &plume_store::RefreshApp,
device: &Device,
) -> Result<(), String> {
// Try to acquire the lock for this UDID.
{
let mut tasks = self
.active_tasks
.lock()
.map_err(|_| "Failed to lock task registry")?;
if tasks.contains(&device.udid) {
log::warn!(
"Refresh already in progress for {}. Aborting duplicate.",
device.udid
);
return Ok(());
}
tasks.insert(device.udid.clone());
}

// lock is released when this function returns
let _guard = RefreshGuard {
udid: device.udid.clone(),
tasks: self.active_tasks.clone(),
};

log::info!("Starting refresh for app at {:?}", app.path);

notify_rust::Notification::new()
.summary("Impactor")
.body(&format!(
"Started refreshing {} for {}",
app.name.as_deref().unwrap_or("???"),
&refresh_device.name
))
.show()
.ok();

let account = store
.get_account(&refresh_device.account)
.ok_or_else(|| format!("Account {} not found", refresh_device.account))?;
Expand Down Expand Up @@ -139,10 +205,15 @@ impl RefreshDaemon {
};

let identity_is_new = {
let identity =
CertificateIdentity::new_with_session(&session, get_data_path(), None, team_id)
.await
.map_err(|e| format!("Failed to create identity: {}", e))?;
let identity = CertificateIdentity::new_with_session(
&session,
get_data_path(),
None,
team_id,
false,
)
.await
.map_err(|e| format!("Failed to create identity: {}", e))?;
identity.new
};

Expand All @@ -155,6 +226,11 @@ impl RefreshDaemon {
false
};

// Determine if we need to reinstall:
// - Mac devices always need reinstalling
// - If the identity is new, we need to reinstall
// - If the app is not installed, we need to reinstall
// - If the app is installed and identity is not new, we can just update profiles
let needs_reinstall = device.is_mac || identity_is_new || !is_installed;

if needs_reinstall {
Expand All @@ -173,6 +249,16 @@ impl RefreshDaemon {

log::info!("Successfully refreshed app at {:?}", app.path);

notify_rust::Notification::new()
.summary("Impactor")
.body(&format!(
"Successfully refreshed {} for {}",
app.name.as_deref().unwrap_or("???"),
&refresh_device.name
))
.show()
.ok();

Ok(())
}

Expand All @@ -198,10 +284,15 @@ impl RefreshDaemon {
};

let team_id_string = team_id.to_string();
let signing_identity =
CertificateIdentity::new_with_session(session, get_data_path(), None, &team_id_string)
.await
.map_err(|e| format!("Failed to create signing identity: {}", e))?;
let signing_identity = CertificateIdentity::new_with_session(
session,
get_data_path(),
None,
&team_id_string,
false,
)
.await
.map_err(|e| format!("Failed to create signing identity: {}", e))?;

let mut signer = Signer::new(Some(signing_identity), options);

Expand Down
Loading