diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dc39fc1..a1dc461 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -149,11 +149,9 @@ jobs: # fmt --all runs on all workspace packages so this is handled by # check_and_test above - name: Check - run: | - SKIP_INSTALLER_COPY=true cargo check --package installer-gui --verbose + run: NO_FIRMWARE_BIN=true cargo check --package installer-gui --verbose - name: Run clippy - run: | - SKIP_INSTALLER_COPY=true cargo clippy --package installer-gui --verbose + run: NO_FIRMWARE_BIN=true cargo clippy --package installer-gui --verbose test_daemon_frontend: needs: files_changed @@ -391,13 +389,10 @@ jobs: - name: Install tauri dependencies run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev xdg-utils - name: Build GUI installer - env: - INSTALLER_PATH: "${{ github.workspace }}/installer-${{ matrix.platform.name }}/installer" shell: bash run: | cd installer-gui npm install - chmod +x "$INSTALLER_PATH" npm run tauri build -- --target ${{ matrix.platform.target }} - uses: actions/upload-artifact@v4 with: @@ -421,7 +416,8 @@ jobs: contents: read packages: write needs: - - build_rust_installer + - build_rayhunter + - build_rootshell - files_changed - installer_gui_check - test_installer_frontend @@ -441,13 +437,10 @@ jobs: targets: ${{ matrix.platform.target }} - uses: Swatinem/rust-cache@v2 - name: Build GUI installer - env: - INSTALLER_PATH: "${{ github.workspace }}/installer-${{ matrix.platform.name }}/installer" shell: bash run: | cd installer-gui npm install - chmod +x "$INSTALLER_PATH" npm run tauri build -- --target ${{ matrix.platform.target }} cd .. mv "target/${{ matrix.platform.target }}/release/bundle/macos/"*.app . @@ -480,12 +473,9 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build GUI installer shell: bash - env: - INSTALLER_PATH: "${{ github.workspace }}/installer-windows-x86_64/installer.exe" run: | cd installer-gui npm install - chmod +x "$INSTALLER_PATH" npm run tauri build -- --target ${{ env.TARGET }} - uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 1cd3acf..d83f3bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /book .DS_Store +installer-gui/src-tauri/gen/ diff --git a/Cargo.lock b/Cargo.lock index c94270a..8203670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2738,6 +2738,7 @@ name = "installer-gui" version = "0.7.1" dependencies = [ "anyhow", + "installer", "serde", "serde_json", "tauri", diff --git a/installer-gui/README.md b/installer-gui/README.md index d679905..033a6f5 100644 --- a/installer-gui/README.md +++ b/installer-gui/README.md @@ -12,16 +12,22 @@ You'll need to install [Tauri's dependencies](https://tauri.app/start/prerequisi ### Rayhunter CLI Installer -The Rayhunter GUI installer currently just bundles and wraps the CLI Rayhunter installer. When building the GUI installer, the CLI installer needs to be built and available for bundling. By default it assumed the installer is present in the repo's `target` directory at either `debug/installer` or `release/installer` depending on whether you're doing a debug or release build of the GUI installer. +The GUI installer pulls in the CLI installer as a library. Like with the CLI installer, the firmware binary needs to be present and can be overridden with the same envvars. See `../installer/build.rs` for options. -You can use a different path by setting the environment variable INSTALLER_PATH when the GUI installer being built. You can also use the environment variable SKIP_INSTALLER_COPY which leaves any previously bundled CLI installer unmodified or if one does not exist bundles a dummy installer file allowing the GUI installer to be successfully built. +For example, to build the firmware in development mode and then provide the path explicitly: + +```bash +cargo build-daemon-firmware-devel + +(cd installer-gui && FILE_RAYHUNTER_DAEMON=$PWD/../target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon npm run tauri android build) +``` ## Building After preparing dependencies, the GUI installer can be built by: 1. Running `npm install` in this directory. -2. Setting INSTALLER_PATH or SKIP_INSTALLER_COPY if desired and running `npm run tauri dev`. +2. Running `npm run tauri dev`. This will build the GUI installer in development mode. While this command is running, any changes to either the frontend or backend code will cause the installer to be reloaded or rebuilt. diff --git a/installer-gui/src-tauri/Cargo.toml b/installer-gui/src-tauri/Cargo.toml index ab37ac5..0a704a2 100644 --- a/installer-gui/src-tauri/Cargo.toml +++ b/installer-gui/src-tauri/Cargo.toml @@ -22,4 +22,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-shell = "2" anyhow = "1.0.100" - +installer = { path = "../../installer" } diff --git a/installer-gui/src-tauri/build.rs b/installer-gui/src-tauri/build.rs index 3528002..d860e1e 100644 --- a/installer-gui/src-tauri/build.rs +++ b/installer-gui/src-tauri/build.rs @@ -1,52 +1,3 @@ -use std::env::consts::EXE_SUFFIX; -use std::path::PathBuf; - fn main() { - println!("cargo::rerun-if-env-changed=INSTALLER_PATH"); - println!("cargo::rerun-if-env-changed=SKIP_INSTALLER_COPY"); - - let destination = get_installer_destination(); - if std::env::var_os("SKIP_INSTALLER_COPY").is_none() { - let cli_installer = - std::env::var_os("INSTALLER_PATH").map_or_else(default_installer_path, PathBuf::from); - if !cli_installer.exists() { - println!( - "cargo::error=CLI installer binary not present at {}", - cli_installer.display() - ); - std::process::exit(0); - } - std::fs::copy(&cli_installer, &destination).unwrap(); - println!("cargo::rerun-if-changed={}", cli_installer.display()); - println!("cargo::rerun-if-changed={}", destination.display()); - } else if !destination.exists() { - // if SKIP_INSTALLER_COPY is set, make sure something exists at destination so the build succeeds - std::fs::write(&destination, []).unwrap(); - } - tauri_build::build() } - -fn default_installer_path() -> PathBuf { - // the approach used here was taken from https://github.com/rust-lang/cargo/issues/9661#issuecomment-1722358176 - let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let profile = std::env::var("PROFILE").unwrap(); - let profile_dir = out_dir - .ancestors() - .find(|&path| path.ends_with(&profile)) - .unwrap(); - - profile_dir.join(format!("installer{EXE_SUFFIX}")) -} - -fn get_installer_destination() -> PathBuf { - // tauri expects included binaries to have the target triple appended to the file name like - // this. see https://tauri.app/develop/sidecar/ - let target_triple = std::env::var("TARGET").unwrap(); - [ - "binaries", - &format!("installer-cli-{target_triple}{EXE_SUFFIX}"), - ] - .iter() - .collect() -} diff --git a/installer-gui/src-tauri/src/lib.rs b/installer-gui/src-tauri/src/lib.rs index a91fd43..89fc561 100644 --- a/installer-gui/src-tauri/src/lib.rs +++ b/installer-gui/src-tauri/src/lib.rs @@ -1,29 +1,18 @@ -use anyhow::Context; use tauri::Emitter; -use tauri_plugin_shell::ShellExt; -use tauri_plugin_shell::process::CommandEvent; async fn run_installer(app_handle: tauri::AppHandle, args: String) -> anyhow::Result<()> { - let (mut rx, _child) = app_handle - .shell() - .sidecar("installer-cli") - .context("Error preparing Rayhunter CLI installer to be run")? - .args(args.split_whitespace()) - .spawn() - .context("Error launching Rayhunter CLI installer")?; - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line_bytes) | CommandEvent::Stderr(line_bytes) => { - let line = String::from_utf8(line_bytes) - .context("Error parsing Rayhunter CLI installer output")?; + tauri::async_runtime::spawn_blocking(move || { + installer::run_with_callback( + // TODO: we should split using something similar to shlex in python + args.split_whitespace().map(String::from).collect(), + Some(Box::new(move |output| { app_handle - .emit("installer-output", &line) - .context("Error sending Rayhunter CLI installer output to GUI frontend")?; - } - _ => (), - }; - } - Ok(()) + .emit("installer-output", output) + .expect("Error sending Rayhunter CLI installer output to GUI frontend"); + })), + ) + }) + .await? } #[tauri::command] diff --git a/installer-gui/src-tauri/tauri.conf.json b/installer-gui/src-tauri/tauri.conf.json index 81152eb..b00dd90 100644 --- a/installer-gui/src-tauri/tauri.conf.json +++ b/installer-gui/src-tauri/tauri.conf.json @@ -23,7 +23,6 @@ "bundle": { "active": true, "targets": ["app", "appimage", "deb", "msi", "nsis", "rpm"], - "externalBin": ["binaries/installer-cli"], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 49a4ca1..9d7fa9a 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -3,6 +3,14 @@ name = "installer" version = "0.8.0" edition = "2024" +[lib] +name = "installer" +crate-type = ["rlib"] + +[[bin]] +name = "installer" +path = "src/main.rs" + [dependencies] aes = "0.8.4" anyhow = "1.0.98" @@ -24,7 +32,7 @@ tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-fe tokio-retry2 = "0.5.7" tokio-stream = "0.1.17" -[target.'cfg(target_os = "linux")'.dependencies.adb_client] +[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies.adb_client] git = "https://github.com/EFForg/adb_client.git" rev = "e511662394e4fa32865c154c40f81a3d846f700c" default-features = false diff --git a/installer/src/lib.rs b/installer/src/lib.rs index 8c11f90..b9558bd 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -1,19 +1,29 @@ -use anyhow::{Context, Error, bail}; +use anyhow::{Context, Error}; use clap::{Parser, Subcommand}; use env_logger::Env; +#[cfg(not(target_os = "android"))] +use anyhow::bail; + +#[cfg(not(target_os = "android"))] mod orbic; mod orbic_auth; mod orbic_network; +mod output; + +#[cfg(not(target_os = "android"))] mod pinephone; mod tmobile; mod tplink; mod util; +#[cfg(not(target_os = "android"))] mod uz801; mod wingtech; -pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in"); -pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon"); +use crate::output::eprintln; + +static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in"); +static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon"); #[derive(Parser, Debug)] #[command(version, about)] @@ -27,6 +37,7 @@ struct Args { #[derive(Subcommand, Debug)] enum Command { /// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer. + #[cfg(not(target_os = "android"))] OrbicUsb(InstallOrbic), /// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network. #[clap(alias = "orbic-network")] @@ -34,8 +45,10 @@ enum Command { /// Install rayhunter on the TMobile TMOHS1. Tmobile(TmobileArgs), /// Install rayhunter on the Uz801. + #[cfg(not(target_os = "android"))] Uz801(Uz801Args), /// Install rayhunter on a PinePhone's Quectel modem. + #[cfg(not(target_os = "android"))] Pinephone(InstallPinephone), /// Install rayhunter on the TP-Link M7350. Tplink(InstallTpLink), @@ -98,14 +111,18 @@ struct Util { #[derive(Subcommand, Debug)] enum UtilSubCommand { /// Send a serial command to the Orbic. + #[cfg(not(target_os = "android"))] Serial(Serial), /// Start an ADB shell + #[cfg(not(target_os = "android"))] Shell, /// Root the Tmobile and launch adb. + #[cfg(not(target_os = "android"))] TmobileStartAdb(TmobileArgs), /// Root the Tmobile and launch telnetd. TmobileStartTelnet(TmobileArgs), /// Root the Uz801 and launch adb. + #[cfg(not(target_os = "android"))] Uz801StartAdb(Uz801Args), /// Root the tplink and launch telnetd. TplinkStartTelnet(TplinkStartTelnet), @@ -114,8 +131,10 @@ enum UtilSubCommand { /// Root the Wingtech and launch adb. WingtechStartAdb(WingtechArgs), /// Unlock the Pinephone's modem and start adb. + #[cfg(not(target_os = "android"))] PinephoneStartAdb, /// Lock the Pinephone's modem and stop adb. + #[cfg(not(target_os = "android"))] PinephoneStopAdb, /// Root the Orbic and launch telnetd. OrbicStartTelnet(OrbicNetworkArgs), @@ -196,20 +215,24 @@ struct Serial { command: Vec, } -async fn run() -> Result<(), Error> { +async fn run(args: Args) -> Result<(), Error> { env_logger::Builder::from_env(Env::default().default_filter_or("off")).init(); - let Args { command } = Args::parse(); - match command { + match args.command { Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, + #[cfg(not(target_os = "android"))] Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?, Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?, + #[cfg(not(target_os = "android"))] Command::Pinephone(_) => pinephone::install().await .context("Failed to install rayhunter on the Pinephone's Quectel modem")?, + #[cfg(not(target_os = "android"))] Command::OrbicUsb(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?, Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).await.context("\nFailed to install rayhunter on the Orbic RC400L")?, Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, - Command::Util(subcommand) => match subcommand.command { + Command::Util(subcommand) => { + match subcommand.command { + #[cfg(not(target_os = "android"))] UtilSubCommand::Serial(serial_cmd) => { if serial_cmd.root { if !serial_cmd.command.is_empty() { @@ -228,9 +251,12 @@ async fn run() -> Result<(), Error> { } } } + #[cfg(not(target_os = "android"))] UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?, UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?, + #[cfg(not(target_os = "android"))] UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?, + #[cfg(not(target_os = "android"))] UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?, UtilSubCommand::TplinkStartTelnet(options) => { tplink::start_telnet(&options.admin_ip).await?; @@ -243,19 +269,75 @@ async fn run() -> Result<(), Error> { } UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?, UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?, + #[cfg(not(target_os = "android"))] UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?, + #[cfg(not(target_os = "android"))] UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?, UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\\nFailed to start telnet on the Orbic RC400L")?, } + } } Ok(()) } -#[tokio::main(flavor = "current_thread")] -async fn main() { - if let Err(e) = run().await { - eprintln!("{e:?}"); - std::process::exit(1); +/// Type alias for output callback function +pub type OutputCallback = Box; + +/// Run the installer with CLI arguments and optional output callback +/// +/// # Arguments +/// * `args` - Command-line arguments (including program name as `args[0]`) +/// * `callback` - Optional function to receive stdout/stderr output +/// +/// # Returns +/// * `Ok(())` on success +/// * `Err(anyhow::Error)` on failure with full error context +/// +/// # Example +/// ```no_run +/// use installer; +/// +/// // if the callback is None, stdout/stderr is going to be used +/// let result = installer::run_with_callback( +/// vec!["installer".to_string(), "orbic-network".to_string(), "--admin-password".to_string(), "12345".to_string()], +/// Some(Box::new(|output| { +/// print!("{}", output); +/// })) +/// ); +/// ``` +pub fn run_with_callback(args: Vec, callback: Option) -> Result<(), Error> { + // Set up the callback if provided + let _guard; + if let Some(cb) = callback { + _guard = output::set_output_callback(move |s: &str| cb(s)); } + + // Create a Tokio runtime and run the installer + + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("Failed to create Tokio runtime")? + .block_on(async { + // Parse arguments + let parsed_args = Args::try_parse_from(&args).context("Failed to parse arguments")?; + + // Run the installer + run(parsed_args).await + }) +} + +/// Get the version of the installer +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +/// Run the CLI installer +/// +/// This function is public so the binary can call it, but library users +/// should use the typed functions like `run_with_callback` instead. +pub async fn main_cli() -> Result<(), Error> { + let args = Args::parse(); + run(args).await } diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index 135d3f0..55fa1bc 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "windows")] use std::io::stdin; -use std::io::{ErrorKind, Write}; +use std::io::ErrorKind; use std::path::Path; use std::time::Duration; @@ -12,7 +12,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; use sha2::{Digest, Sha256}; use tokio::time::sleep; -use crate::util::{echo, open_usb_device}; +use crate::output::{print, println}; +use crate::util::open_usb_device; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found. @@ -54,7 +55,7 @@ const RNDIS_INTERFACE: u8 = 1; #[cfg(target_os = "windows")] async fn confirm() -> Result { println!("{}", WINDOWS_WARNING); - echo!("Do you wish to proceed? Enter 'yes' to install> "); + print!("Do you wish to proceed? Enter 'yes' to install> "); let mut input = String::new(); stdin().read_line(&mut input)?; Ok(input.trim() == "yes") @@ -75,13 +76,13 @@ pub async fn install() -> Result<()> { } let mut adb_device = force_debug_mode().await?; - echo!("Installing rootshell... "); + print!("Installing rootshell... "); setup_rootshell(&mut adb_device).await?; println!("done"); - echo!("Installing rayhunter... "); + print!("Installing rayhunter... "); let mut adb_device = setup_rayhunter(adb_device).await?; println!("done"); - echo!("Testing rayhunter... "); + print!("Testing rayhunter... "); test_rayhunter(&mut adb_device).await?; println!("done"); Ok(()) @@ -101,11 +102,11 @@ pub async fn shell() -> Result<()> { async fn force_debug_mode() -> Result { println!("Forcing a switch into the debug mode to enable ADB"); enable_command_mode()?; - echo!("ADB enabled, waiting for reboot... "); + print!("ADB enabled, waiting for reboot... "); let mut adb_device = get_adb().await?; adb_setup_serial(&mut adb_device).await?; println!("it's alive!"); - echo!("Waiting for atfwd_daemon to startup... "); + print!("Waiting for atfwd_daemon to startup... "); adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?; println!("done"); Ok(adb_device) @@ -159,7 +160,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result { adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?; adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?; println!("done"); - echo!("Waiting for reboot... "); + print!("Waiting for reboot... "); adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?; // first wait for shutdown (it can take ~10s) tokio::time::timeout(Duration::from_secs(30), async { diff --git a/installer/src/orbic_network.rs b/installer/src/orbic_network.rs index 2771144..b66c7b5 100644 --- a/installer/src/orbic_network.rs +++ b/installer/src/orbic_network.rs @@ -1,4 +1,3 @@ -use std::io::Write; use std::net::SocketAddr; use std::str::FromStr; use std::time::Duration; @@ -9,7 +8,8 @@ use serde::Deserialize; use tokio::time::sleep; use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password}; -use crate::util::{echo, telnet_send_command, telnet_send_file}; +use crate::output::{eprintln, print, println}; +use crate::util::{telnet_send_command, telnet_send_file}; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; #[derive(Deserialize, Debug)] @@ -128,7 +128,7 @@ pub async fn start_telnet( anyhow::bail!("--admin-password is required"); }; - echo!("Logging in and starting telnet... "); + print!("Logging in and starting telnet... "); login_and_exploit(admin_ip, admin_username, admin_password).await?; println!("done"); @@ -154,11 +154,11 @@ pub async fn install( anyhow::bail!("exiting"); }; - echo!("Logging in and starting telnet... "); + print!("Logging in and starting telnet... "); login_and_exploit(&admin_ip, &admin_username, &admin_password).await?; println!("done"); - echo!("Waiting for telnet to become available... "); + print!("Waiting for telnet to become available... "); wait_for_telnet(&admin_ip).await?; println!("done"); diff --git a/installer/src/output.rs b/installer/src/output.rs new file mode 100644 index 0000000..11b60ee --- /dev/null +++ b/installer/src/output.rs @@ -0,0 +1,112 @@ +//! Output handling for the installer +//! +//! This module provides custom print macros that can be intercepted by setting +//! a callback function. This is essential for FFI usage where stdout/stderr +//! redirection doesn't work reliably (especially on Android). + +use std::io::Write; +use std::sync::Mutex; + +/// Type for the output callback function +type OutputCallbackFn = Box; + +/// Global output callback storage +static OUTPUT_CALLBACK: Mutex> = Mutex::new(None); + +/// Set the global output callback +/// +/// All output from `println!` and `eprintln!` will be sent to this callback. +/// If no callback is set, output goes to stdout/stderr as normal. +/// +/// Returns a guard that when dropped, resets the callback. +pub(crate) fn set_output_callback(callback: F) -> OutputCallbackGuard +where + F: Fn(&str) + Send + Sync + 'static, +{ + *OUTPUT_CALLBACK.lock().unwrap() = Some(Box::new(callback)); + OutputCallbackGuard +} + +pub struct OutputCallbackGuard; + +impl Drop for OutputCallbackGuard { + fn drop(&mut self) { + clear_output_callback(); + } +} + +/// Clear the global output callback +pub(crate) fn clear_output_callback() { + *OUTPUT_CALLBACK.lock().unwrap() = None; +} + +/// Write a line to the output (either callback or stdout) +pub(crate) fn write_output_line(s: &str) { + if let Ok(guard) = OUTPUT_CALLBACK.lock() + && let Some(ref callback) = *guard + { + callback(s); + callback("\n"); + return; + } + // Fallback to stdout if no callback or lock failed + std::println!("{}", s); + let _ = std::io::stdout().flush(); +} + +/// Write an error line to the output (either callback or stderr) +pub(crate) fn write_error_line(s: &str) { + if let Ok(guard) = OUTPUT_CALLBACK.lock() + && let Some(ref callback) = *guard + { + callback(s); + callback("\n"); + return; + } + // Fallback to stderr if no callback or lock failed + std::eprintln!("{}", s); + let _ = std::io::stderr().flush(); +} + +/// Write raw output without newline (either callback or stdout) +pub(crate) fn write_output_raw(s: &str) { + if let Ok(guard) = OUTPUT_CALLBACK.lock() + && let Some(ref callback) = *guard + { + callback(s); + return; + } + // Fallback to stdout if no callback or lock failed + std::print!("{}", s); + let _ = std::io::stdout().flush(); +} + +/// Shadow println! macro to respect the output callback +macro_rules! println { + () => { + $crate::output::write_output_line("") + }; + ($($arg:tt)*) => {{ + $crate::output::write_output_line(&format!($($arg)*)) + }}; +} +pub(crate) use println; + +/// Shadow eprintln! macro to respect the output callback +macro_rules! eprintln { + () => { + $crate::output::write_error_line("") + }; + ($($arg:tt)*) => {{ + $crate::output::write_error_line(&format!($($arg)*)) + }}; +} +pub(crate) use eprintln; + +/// Shadow print! macro to respect the output callback +macro_rules! print { + ($($arg:tt)*) => {{ + $crate::output::write_output_raw(&format!($($arg)*)) + }}; +} +pub(crate) use print; diff --git a/installer/src/pinephone.rs b/installer/src/pinephone.rs index c988b36..11350b4 100644 --- a/installer/src/pinephone.rs +++ b/installer/src/pinephone.rs @@ -1,4 +1,3 @@ -use std::io::Write; use std::path::Path; use std::time::Duration; @@ -11,7 +10,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; use tokio::time::sleep; use crate::orbic::test_rayhunter; -use crate::util::{echo, open_usb_device}; +use crate::output::{print, println}; +use crate::util::open_usb_device; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; const USB_VENDOR_ID: u16 = 0x2C7C; @@ -19,7 +19,7 @@ const USB_PRODUCT_ID: u16 = 0x125; const USB_INTERFACE_NUMBER: u8 = 2; pub async fn install() -> Result<()> { - echo!("Unlocking modem ... "); + print!("Unlocking modem ... "); start_adb().await?; sleep(Duration::from_secs(3)).await; let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap(); @@ -54,13 +54,13 @@ pub async fn install() -> Result<()> { adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?; sleep(Duration::from_secs(30)).await; - echo!("Unlocking modem ... "); + print!("Unlocking modem ... "); start_adb().await?; sleep(Duration::from_secs(3)).await; let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap(); println!("ok"); - echo!("Testing rayhunter ... "); + print!("Testing rayhunter ... "); test_rayhunter(&mut adb).await?; println!("ok"); println!("rayhunter is running on the modem. Use adb to access the web interface."); @@ -198,7 +198,7 @@ impl Install for ADBUSBDevice { /// Transfer a file to the modem's filesystem with adb push. /// Validates the file sends successfully to /tmp before overwriting the destination. fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> { - echo!("Sending file {dest} ... "); + print!("Sending file {dest} ... "); let file_name = Path::new(dest) .file_name() .ok_or_else(|| anyhow!("{dest} does not have a file name"))? diff --git a/installer/src/tmobile.rs b/installer/src/tmobile.rs index 0fd47da..00c19e1 100644 --- a/installer/src/tmobile.rs +++ b/installer/src/tmobile.rs @@ -4,7 +4,6 @@ /// WT_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP /// WT_PRODUCTION_VERSION=TMOHS1_00.05.20 /// WT_HARDWARE_VERSION=89527_1_11 -use std::io::Write; use std::net::SocketAddr; use std::str::FromStr; use std::time::Duration; @@ -13,7 +12,8 @@ use anyhow::Result; use tokio::time::sleep; use crate::TmobileArgs as Args; -use crate::util::{echo, http_ok_every, telnet_send_command, telnet_send_file}; +use crate::output::{print, println}; +use crate::util::{http_ok_every, telnet_send_command, telnet_send_file}; use crate::wingtech::start_telnet; pub async fn install( @@ -26,12 +26,12 @@ pub async fn install( } async fn run_install(admin_ip: String, admin_password: String) -> Result<()> { - echo!("Starting telnet ... "); + print!("Starting telnet ... "); start_telnet(&admin_ip, &admin_password).await?; sleep(Duration::from_millis(200)).await; println!("ok"); - echo!("Connecting via telnet to {admin_ip} ... "); + print!("Connecting via telnet to {admin_ip} ... "); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?; println!("ok"); @@ -96,7 +96,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> { telnet_send_command(addr, "reboot", "exit code 0", true).await?; sleep(Duration::from_secs(30)).await; - echo!("Testing rayhunter ... "); + print!("Testing rayhunter ... "); let max_failures = 10; http_ok_every( format!("http://{admin_ip}:8080/index.html"), diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index 66bf5e1..a3315ed 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -18,6 +18,7 @@ use serde::Deserialize; use tokio::time::sleep; use crate::InstallTpLink; +use crate::output::println; use crate::util::{telnet_send_command, telnet_send_file}; type HttpProxyClient = hyper_util::client::legacy::Client; diff --git a/installer/src/util.rs b/installer/src/util.rs index 0ded952..55b4ba3 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -1,4 +1,3 @@ -use std::io::Write; use std::net::SocketAddr; use std::str::FromStr; use std::time::Duration; @@ -10,13 +9,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::{sleep, timeout}; -macro_rules! echo { - ($($arg:tt)*) => { - print!($($arg)*); - let _ = std::io::stdout().flush(); - }; -} -pub(crate) use echo; +use crate::output::{print, println}; pub async fn telnet_send_command_with_output( addr: SocketAddr, @@ -91,7 +84,7 @@ pub async fn telnet_send_file( payload: &[u8], wait_for_prompt: bool, ) -> Result<()> { - echo!("Sending file {filename}... "); + print!("Sending file {filename} ... "); let nc_output = { let filename = filename.to_owned(); let handle = tokio::spawn(async move { @@ -122,7 +115,7 @@ pub async fn telnet_send_file( break; } - echo!("attempt {attempts}... "); + print!("attempt {attempts}... "); } { @@ -216,6 +209,7 @@ pub async fn http_ok_every( } /// General function to open a USB device +#[cfg(not(target_os = "android"))] pub fn open_usb_device(vid: u16, pid: u16) -> Result> { let devices = match nusb::list_devices() { Ok(d) => d, diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs index d3b9528..7ad0321 100644 --- a/installer/src/uz801.rs +++ b/installer/src/uz801.rs @@ -1,4 +1,3 @@ -use std::io::Write; use std::path::Path; /// Installer for the Uz801 hotspot. /// @@ -15,30 +14,30 @@ use md5::compute as md5_compute; use tokio::time::sleep; use crate::Uz801Args as Args; -use crate::util::echo; +use crate::output::{print, println}; pub async fn install(Args { admin_ip }: Args) -> Result<()> { run_install(admin_ip).await } async fn run_install(admin_ip: String) -> Result<()> { - echo!("Activating USB debugging backdoor... "); + print!("Activating USB debugging backdoor... "); activate_usb_debug(&admin_ip).await?; println!("ok"); - echo!("Waiting for device reboot and ADB connection... "); + print!("Waiting for device reboot and ADB connection... "); let mut adb_device = wait_for_adb().await?; println!("ok"); - echo!("Installing rayhunter files... "); + print!("Installing rayhunter files... "); install_rayhunter_files(&mut adb_device).await?; println!("ok"); - echo!("Modifying startup script... "); + print!("Modifying startup script... "); modify_startup_script(&mut adb_device).await?; println!("ok"); - echo!("Rebooting the device... "); + print!("Rebooting the device... "); let _ = adb_device.reboot(adb_client::RebootType::System); println!("ok"); @@ -55,7 +54,7 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> { let origin = format!("http://{admin_ip}"); // Check if device is online - echo!("Checking if device is online... "); + print!("Checking if device is online... "); let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build()?; diff --git a/installer/src/wingtech.rs b/installer/src/wingtech.rs index f341144..ae84e46 100644 --- a/installer/src/wingtech.rs +++ b/installer/src/wingtech.rs @@ -4,7 +4,6 @@ /// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP /// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55 /// WT_HARDWARE_VERSION=89323_1_20 -use std::io::Write; use std::net::SocketAddr; use std::str::FromStr; use std::time::Duration; @@ -19,7 +18,8 @@ use serde::Deserialize; use tokio::time::sleep; use crate::WingtechArgs as Args; -use crate::util::{echo, http_ok_every, telnet_send_command, telnet_send_file}; +use crate::output::{print, println}; +use crate::util::{http_ok_every, telnet_send_command, telnet_send_file}; #[derive(Deserialize)] struct LoginResponse { @@ -89,11 +89,11 @@ pub async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Res } async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Result<()> { - echo!("Starting telnet ... "); + print!("Starting telnet ... "); start_telnet(&admin_ip, &admin_password).await?; println!("ok"); - echo!("Connecting via telnet to {admin_ip} ... "); + print!("Connecting via telnet to {admin_ip} ... "); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?; println!("ok"); @@ -149,7 +149,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0", true).await?; sleep(Duration::from_secs(30)).await; - echo!("Testing rayhunter ... "); + print!("Testing rayhunter ... "); let max_failures = 10; http_ok_every( format!("http://{admin_ip}:8080/index.html"),