diff --git a/installer/src/connection.rs b/installer/src/connection.rs new file mode 100644 index 0000000..e77538a --- /dev/null +++ b/installer/src/connection.rs @@ -0,0 +1,68 @@ +use std::future::Future; +use std::net::SocketAddr; + +use anyhow::Result; + +use crate::output::println; + +/// Abstraction for device communication (telnet or ADB) +pub trait DeviceConnection { + /// Run a shell command and return its output + fn run_command(&mut self, command: &str) -> impl Future> + Send; + + /// Write a file to the device + fn write_file(&mut self, path: &str, content: &[u8]) + -> impl Future> + Send; +} + +/// Check if a file exists using a DeviceConnection +pub async fn file_exists(conn: &mut C, path: &str) -> bool { + conn.run_command(&format!("test -f {path} && echo exists || echo missing")) + .await + .map(|output| output.contains("exists")) + .unwrap_or(false) +} + +/// Shared config installation logic +pub async fn install_config( + conn: &mut C, + config_path: &str, + device_type: &str, + reset_config: bool, +) -> Result<()> { + if reset_config || !file_exists(conn, config_path).await { + let config = crate::CONFIG_TOML.replace( + r#"#device = "orbic""#, + &format!(r#"device = "{device_type}""#), + ); + conn.write_file(config_path, config.as_bytes()).await?; + } else { + println!("Config file already exists, skipping (use --reset-config to overwrite)"); + } + Ok(()) +} + +/// Telnet-based connection wrapper +pub struct TelnetConnection { + pub addr: SocketAddr, + pub wait_for_prompt: bool, +} + +impl TelnetConnection { + pub fn new(addr: SocketAddr, wait_for_prompt: bool) -> Self { + Self { + addr, + wait_for_prompt, + } + } +} + +impl DeviceConnection for TelnetConnection { + async fn run_command(&mut self, command: &str) -> Result { + crate::util::telnet_send_command_with_output(self.addr, command, self.wait_for_prompt).await + } + + async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> { + crate::util::telnet_send_file(self.addr, path, content, self.wait_for_prompt).await + } +} diff --git a/installer/src/lib.rs b/installer/src/lib.rs index 6db79d8..3b6fee3 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -5,6 +5,7 @@ use env_logger::Env; #[cfg(not(target_os = "android"))] use anyhow::bail; +mod connection; #[cfg(not(target_os = "android"))] mod orbic; mod orbic_auth; @@ -79,10 +80,18 @@ struct InstallTpLink { /// your custom path may conflict with the builtin storage functionality. #[arg(long, default_value = "")] sdcard_path: String, + + /// Overwrite config.toml even if it already exists on the device. + #[arg(long)] + reset_config: bool, } #[derive(Parser, Debug)] -struct InstallOrbic {} +struct InstallOrbic { + /// Overwrite config.toml even if it already exists on the device. + #[arg(long)] + reset_config: bool, +} #[derive(Parser, Debug)] struct OrbicNetworkArgs { @@ -97,6 +106,10 @@ struct OrbicNetworkArgs { /// Admin password for authentication. #[arg(long)] admin_password: Option, + + /// Overwrite config.toml even if it already exists on the device. + #[arg(long)] + reset_config: bool, } #[derive(Parser, Debug)] @@ -231,8 +244,8 @@ async fn run(args: Args) -> Result<(), Error> { 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::OrbicUsb(args) => orbic::install(args.reset_config).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, args.reset_config).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 { diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index 1e3d7e2..a414e4b 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -12,9 +12,10 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; use sha2::{Digest, Sha256}; use tokio::time::sleep; +use crate::RAYHUNTER_DAEMON_INIT; +use crate::connection::{DeviceConnection, install_config}; 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. Make sure your device is plugged in and turned on. @@ -46,6 +47,21 @@ const PRODUCT_ID: u16 = 0xf601; const INTERFACE: u8 = 1; +/// ADB-based connection wrapper for DeviceConnection trait +pub struct AdbConnection<'a> { + device: &'a mut ADBUSBDevice, +} + +impl DeviceConnection for AdbConnection<'_> { + async fn run_command(&mut self, command: &str) -> Result { + adb_command(self.device, &["sh", "-c", command]) + } + + async fn write_file(&mut self, path: &str, content: &[u8]) -> Result<()> { + install_file(self.device, path, content).await + } +} + #[cfg(target_os = "windows")] const RNDIS_INTERFACE: u8 = 0; @@ -61,7 +77,7 @@ async fn confirm() -> Result { Ok(input.trim() == "yes") } -pub async fn install() -> Result<()> { +pub async fn install(reset_config: bool) -> Result<()> { println!( "WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer orbic instead, unless you want ADB access for other purposes." ); @@ -80,7 +96,7 @@ pub async fn install() -> Result<()> { setup_rootshell(&mut adb_device).await?; println!("done"); print!("Installing rayhunter... "); - let mut adb_device = setup_rayhunter(adb_device).await?; + let mut adb_device = setup_rayhunter(adb_device, reset_config).await?; println!("done"); print!("Testing rayhunter... "); test_rayhunter(&mut adb_device).await?; @@ -127,7 +143,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> { Ok(()) } -async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result { +async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result { let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?; @@ -137,14 +153,20 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result { rayhunter_daemon_bin, ) .await?; - install_file( - &mut adb_device, - "/data/rayhunter/config.toml", - CONFIG_TOML - .replace("#device = \"orbic\"", "device = \"orbic\"") - .as_bytes(), - ) - .await?; + + { + let mut conn = AdbConnection { + device: &mut adb_device, + }; + install_config( + &mut conn, + "/data/rayhunter/config.toml", + "orbic", + reset_config, + ) + .await?; + } + install_file( &mut adb_device, "/etc/init.d/rayhunter_daemon", diff --git a/installer/src/orbic_network.rs b/installer/src/orbic_network.rs index bc4e068..44d4d26 100644 --- a/installer/src/orbic_network.rs +++ b/installer/src/orbic_network.rs @@ -7,10 +7,11 @@ use reqwest::Client; use serde::Deserialize; use tokio::time::sleep; +use crate::RAYHUNTER_DAEMON_INIT; +use crate::connection::{TelnetConnection, install_config}; use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password}; use crate::output::{eprintln, print, println}; use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; -use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; // Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case const TELNET_PORT: u16 = 24; @@ -142,6 +143,7 @@ pub async fn install( admin_ip: String, admin_username: String, admin_password: Option, + reset_config: bool, ) -> Result<()> { let Some(admin_password) = admin_password else { eprintln!( @@ -165,7 +167,7 @@ pub async fn install( wait_for_telnet(&admin_ip).await?; println!("done"); - setup_rayhunter(&admin_ip).await + setup_rayhunter(&admin_ip, reset_config).await } async fn wait_for_telnet(admin_ip: &str) -> Result<()> { @@ -189,7 +191,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> { Ok(()) } -async fn setup_rayhunter(admin_ip: &str) -> Result<()> { +async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> { let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); @@ -213,13 +215,12 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> { ) .await?; - telnet_send_file( - addr, + let mut conn = TelnetConnection::new(addr, false); + install_config( + &mut conn, "/data/rayhunter/config.toml", - CONFIG_TOML - .replace(r#"#device = "orbic""#, r#"device = "orbic""#) - .as_bytes(), - false, + "orbic", + reset_config, ) .await?; diff --git a/installer/src/pinephone.rs b/installer/src/pinephone.rs index 11350b4..dae9d8c 100644 --- a/installer/src/pinephone.rs +++ b/installer/src/pinephone.rs @@ -9,6 +9,7 @@ use nusb::Interface; use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; use tokio::time::sleep; +use crate::connection::DeviceConnection; use crate::orbic::test_rayhunter; use crate::output::{print, println}; use crate::util::open_usb_device; @@ -25,33 +26,39 @@ pub async fn install() -> Result<()> { let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap(); println!("ok"); - adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?; - adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?; + run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?; + run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); - adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?; - adb.install_file( + adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin) + .await?; + adb.write_file( "/data/rayhunter/config.toml", CONFIG_TOML .replace("#device = \"orbic\"", "device = \"pinephone\"") .as_bytes(), - )?; - adb.install_file( + ) + .await?; + adb.write_file( "/etc/init.d/rayhunter_daemon", RAYHUNTER_DAEMON_INIT.as_bytes(), - )?; - adb.install_file( + ) + .await?; + adb.write_file( "/etc/init.d/misc-daemon", include_bytes!("../../dist/scripts/misc-daemon"), - )?; - adb.run_command( - &["chmod", "755", "/etc/init.d/rayhunter_daemon"], + ) + .await?; + run_command_expect( + &mut adb, + "chmod 755 /etc/init.d/rayhunter_daemon", "exit code 0", - )?; - adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?; + ) + .await?; + run_command_expect(&mut adb, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?; println!("Rebooting device and waiting 30 seconds for it to start up."); - adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?; + run_command_expect(&mut adb, "shutdown -r -t 1 now", "exit code 0").await?; sleep(Duration::from_secs(30)).await; print!("Unlocking modem ... "); @@ -68,6 +75,19 @@ pub async fn install() -> Result<()> { Ok(()) } +/// Helper to run a command and check for expected output +async fn run_command_expect( + adb: &mut ADBUSBDevice, + command: &str, + expected_output: &str, +) -> Result<()> { + let output = adb.run_command(command).await?; + if !output.contains(expected_output) { + bail!("{expected_output:?} not found in: {output}"); + } + Ok(()) +} + struct Qusbcfg { vendor_id: u16, product_id: u16, @@ -175,29 +195,18 @@ pub async fn stop_adb() -> Result<()> { Ok(()) } -trait Install { - fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()>; - fn install_file(&mut self, dest: &str, payload: &[u8]) -> Result<()>; -} - -impl Install for ADBUSBDevice { - /// Run an adb shell command, append '; echo exit code $?' to the command and verify its output. - fn run_command(&mut self, command: &[&str], expected_output: &str) -> Result<()> { +impl DeviceConnection for ADBUSBDevice { + /// Run an adb shell command, append '; echo exit code $?' to the command and return output. + async fn run_command(&mut self, command: &str) -> Result { let mut buf = Vec::::new(); - let mut cmd = Vec::<&str>::new(); - cmd.extend_from_slice(command); - cmd.extend_from_slice(&[";", "echo", "exit code $?"]); + let cmd = ["sh", "-c", &format!("{command}; echo exit code $?")]; self.shell_command(&cmd, &mut buf)?; - let output = String::from_utf8_lossy(&buf); - if !output.contains(expected_output) { - bail!("{expected_output:?} not found in: {output}"); - } - Ok(()) + Ok(String::from_utf8_lossy(&buf).into_owned()) } /// 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<()> { + async fn write_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> { print!("Sending file {dest} ... "); let file_name = Path::new(dest) .file_name() @@ -208,8 +217,16 @@ impl Install for ADBUSBDevice { let push_tmp_path = format!("/tmp/{file_name}"); let file_hash = md5_compute(payload); self.push(&mut payload, &push_tmp_path)?; - self.run_command(&["md5sum", &push_tmp_path], &format!("{file_hash:x}"))?; - self.run_command(&["mv", &push_tmp_path, dest], "exit code 0")?; + let output = self.run_command(&format!("md5sum {push_tmp_path}")).await?; + if !output.contains(&format!("{file_hash:x}")) { + bail!("{:x} not found in: {output}", file_hash); + } + let output = self + .run_command(&format!("mv {push_tmp_path} {dest}")) + .await?; + if !output.contains("exit code 0") { + bail!("exit code 0 not found in: {output}"); + } println!("ok"); Ok(()) } diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index 47c15e2..efbf3d4 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::connection::{TelnetConnection, install_config}; use crate::output::println; use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; @@ -28,10 +29,11 @@ pub async fn main_tplink( skip_sdcard, admin_ip, sdcard_path, + reset_config, }: InstallTpLink, ) -> Result<(), Error> { let is_v3 = start_telnet(&admin_ip).await?; - tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await + tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3, reset_config).await } #[derive(Deserialize)] @@ -111,6 +113,7 @@ async fn tplink_run_install( admin_ip: String, mut sdcard_path: String, is_v3: bool, + reset_config: bool, ) -> Result<(), Error> { println!("Connecting via telnet to {admin_ip}"); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); @@ -181,15 +184,9 @@ async fn tplink_run_install( ) .await?; - telnet_send_file( - addr, - &format!("{sdcard_path}/config.toml"), - crate::CONFIG_TOML - .replace("#device = \"orbic\"", "device = \"tplink\"") - .as_bytes(), - true, - ) - .await?; + let mut conn = TelnetConnection::new(addr, true); + let config_path = format!("{sdcard_path}/config.toml"); + install_config(&mut conn, &config_path, "tplink", reset_config).await?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));