use std::io::Write; use std::path::Path; use std::time::Duration; use adb_client::{ADBDeviceExt, ADBUSBDevice}; use anyhow::{Context, Result, anyhow, bail}; use md5::compute as md5_compute; use md5crypt::md5crypt; use nusb::Interface; 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::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; const USB_VENDOR_ID: u16 = 0x2C7C; const USB_PRODUCT_ID: u16 = 0x125; const USB_INTERFACE_NUMBER: u8 = 2; pub async fn install() -> Result<()> { echo!("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"); adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?; adb.run_command(&["mkdir", "-p", "/data/rayhunter"], "exit code 0")?; let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); adb.install_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)?; adb.install_file( "/data/rayhunter/config.toml", CONFIG_TOML .replace("#device = \"orbic\"", "device = \"pinephone\"") .as_bytes(), )?; adb.install_file( "/etc/init.d/rayhunter_daemon", RAYHUNTER_DAEMON_INIT.as_bytes(), )?; adb.install_file( "/etc/init.d/misc-daemon", include_bytes!("../../dist/scripts/misc-daemon"), )?; adb.run_command( &["chmod", "755", "/etc/init.d/rayhunter_daemon"], "exit code 0", )?; adb.run_command(&["chmod", "755", "/etc/init.d/misc-daemon"], "exit code 0")?; println!("Rebooting device and waiting 30 seconds for it to start up."); adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?; sleep(Duration::from_secs(30)).await; echo!("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 ... "); test_rayhunter(&mut adb).await?; println!("ok"); println!("rayhunter is running on the modem. Use adb to access the web interface."); Ok(()) } struct Qusbcfg { vendor_id: u16, product_id: u16, diag: u8, nmea: u8, at: u8, modem: u8, net: u8, adb: u8, audio: u8, } impl Default for Qusbcfg { fn default() -> Self { Qusbcfg { vendor_id: USB_VENDOR_ID, product_id: USB_PRODUCT_ID, diag: 1, nmea: 1, at: 1, modem: 1, net: 1, adb: 0, audio: 0, } } } impl std::fmt::Display for Qusbcfg { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.write_str(&format!( "AT+QCFG=\"usbcfg\",{:#X},{:#X},{},{},{},{},{},{},{}", self.vendor_id, self.product_id, self.diag, self.nmea, self.at, self.modem, self.net, self.adb, self.audio ))?; Ok(()) } } /// Start the adb daemon on the Quectel modem. /// A reimplementation of qadbkey-unlock.c by "igem, 2019 ;)" pub async fn start_adb() -> Result<()> { let tty = serial_interface()?.unwrap(); let get_qadbkey = tty .send_at_command("AT+QADBKEY?") .await .context("Failed to request QADBKEY")?; let resp = String::from_utf8_lossy(&get_qadbkey); if !resp.contains("\r\nOK\r\n") { bail!("Received unexpected response: {0}", resp); } let salt = match resp.find("+QADBKEY: ") { Some(i) => &resp[i + 10..i + 18], None => bail!("Received unexpected response: {0}", resp), }; let hashed = &md5crypt(b"SH_adb_quectel", salt.as_bytes())[12..28]; let hashed = String::from_utf8_lossy(hashed); let unlock = tty .send_at_command(&format!("AT+QADBKEY=\"{hashed}\"")) .await .context("Failed to send AT+QADBKEY")?; let resp = String::from_utf8_lossy(&unlock); if !resp.contains("\r\nOK\r\n") { bail!("Received unexpected response: {0}", resp); } let adb_enable = Qusbcfg { adb: 1, ..Default::default() }; let start_adb = tty .send_at_command(&adb_enable.to_string()) .await .context("Failed to send enable adb command.")?; let resp = String::from_utf8_lossy(&start_adb); if !resp.contains("\r\nOK\r\n") { bail!("Received unexpected response: {0}", resp); } Ok(()) } /// Stop the adb daemon on the Quectel modem. pub async fn stop_adb() -> Result<()> { let tty = serial_interface()?.unwrap(); let adb_disable = Qusbcfg::default(); let stop_adb = tty .send_at_command(&adb_disable.to_string()) .await .context("Failed to disable adb.")?; let resp = String::from_utf8_lossy(&stop_adb); if !resp.contains("\r\nOK\r\n") { bail!("Received unexpected response: {0}", resp); } 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<()> { let mut buf = Vec::::new(); let mut cmd = Vec::<&str>::new(); cmd.extend_from_slice(command); cmd.extend_from_slice(&[";", "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(()) } /// 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} ... "); let file_name = Path::new(dest) .file_name() .ok_or_else(|| anyhow!("{dest} does not have a file name"))? .to_str() .ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))? .to_owned(); 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")?; println!("ok"); Ok(()) } } /// Claim the modem's USB interface for sending AT commands. fn serial_interface() -> Result> { if let Some(device) = open_usb_device(USB_VENDOR_ID, USB_PRODUCT_ID)? { let interface = device .detach_and_claim_interface(USB_INTERFACE_NUMBER) .context("detach_and_claim_interface({USB_INTERFACE_NUMBER}) failed")?; return Ok(Some(interface)); } Ok(None) } trait AT { async fn send_at_command(&self, command: &str) -> Result>; } impl AT for Interface { /// Send an AT command to the Quectel modem. async fn send_at_command(&self, command: &str) -> Result> { let mut data = String::new(); data.push_str("\r\n"); data.push_str(command); data.push_str("\r\n"); let timeout = Duration::from_secs(1); let enable_serial_port = Control { control_type: ControlType::Class, recipient: Recipient::Interface, request: 0x22, value: 3, index: USB_INTERFACE_NUMBER as u16, }; self.control_out_blocking(enable_serial_port, &[], timeout) .context("Failed to send control request")?; tokio::time::timeout(timeout, self.bulk_out(0x3, data.as_bytes().to_vec())) .await .context("Timed out writing command")? .into_result() .context("Failed to write command")?; let response = tokio::time::timeout(timeout, self.bulk_in(0x84, RequestBuffer::new(256))) .await .context("Timed out reading response")? .into_result() .context("Failed to read response")?; Ok(response) } } #[test] fn test_qadbcfg_fmt() { assert_eq!( Qusbcfg::default().to_string(), "AT+QCFG=\"usbcfg\",0x2C7C,0x125,1,1,1,1,1,0,0" ); }