Files
rayhunter/installer/src/pinephone.rs
2025-07-24 09:48:41 -07:00

277 lines
8.5 KiB
Rust

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::<u8>::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<Option<Interface>> {
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<Vec<u8>>;
}
impl AT for Interface {
/// Send an AT command to the Quectel modem.
async fn send_at_command(&self, command: &str) -> Result<Vec<u8>> {
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"
);
}