Skip to content
Open
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
5 changes: 4 additions & 1 deletion doc/moxee.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL),
Connect to the hotspot's network using WiFi or USB tethering and run:

```sh
./installer orbic --admin-password 'mypassword'
./installer moxee --admin-password 'mypassword'
```

The password (in place of `mypassword`) is under the battery.

`./installer moxee` is almost the same as `./installer orbic`, it just comes
with slightly better defaults that will give you more space for recordings.

## Obtaining a shell

```sh
Expand Down
123 changes: 118 additions & 5 deletions installer/src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::future::Future;
use std::net::SocketAddr;

use anyhow::Result;
use anyhow::{Result, bail};

use crate::output::println;
use crate::output::{print, println};

/// Abstraction for device communication (telnet or ADB)
pub trait DeviceConnection {
Expand All @@ -17,19 +17,20 @@ pub trait DeviceConnection {

/// Check if a file exists using a DeviceConnection
pub async fn file_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
conn.run_command(&format!("test -f {path} && echo exists || echo missing"))
conn.run_command(&format!("test -f '{path}' && echo exists || echo missing"))
.await
.map(|output| output.contains("exists"))
.unwrap_or(false)
}

/// Shared config installation logic
/// Shared config installation logic. Installs to /data/rayhunter/config.toml which resolves
/// through the symlink to the actual data directory.
pub async fn install_config<C: DeviceConnection>(
conn: &mut C,
config_path: &str,
device_type: &str,
reset_config: bool,
) -> Result<()> {
let config_path = "/data/rayhunter/config.toml";
if reset_config || !file_exists(conn, config_path).await {
let config = crate::CONFIG_TOML.replace(
r#"#device = "orbic""#,
Expand All @@ -42,6 +43,118 @@ pub async fn install_config<C: DeviceConnection>(
Ok(())
}

/// Check if a directory exists using a DeviceConnection
pub async fn dir_exists<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
conn.run_command(&format!("test -d '{path}' && echo exists || echo missing"))
.await
.map(|output| output.contains("exists"))
.unwrap_or(false)
}

/// Check if a path is a symlink using a DeviceConnection
pub async fn is_symlink<C: DeviceConnection>(conn: &mut C, path: &str) -> bool {
conn.run_command(&format!("test -L '{path}' && echo yes || echo no"))
.await
.map(|output| output.contains("yes"))
.unwrap_or(false)
}

/// Read the target of a symlink using a DeviceConnection
pub async fn readlink<C: DeviceConnection>(conn: &mut C, path: &str) -> Result<String> {
// Use a prefix marker to find the actual output line, since some shells (TP-Link) echo
// back the command and run_command appends protocol lines.
let output = conn
.run_command(&format!("echo RL:$(readlink '{path}')"))
.await?;

for line in output.lines() {
if let Some(target) = line.trim().strip_prefix("RL:") {
return Ok(target.to_string());
}
}

bail!("unexpected readlink output: {output:?}");
}

/// Set up the data directory at `data_dir` and create a symlink from `/data/rayhunter` to it.
///
/// Handles migration from old locations:
/// - If `/data/rayhunter` is a real directory, moves its contents to `data_dir`
/// - If `/data/rayhunter` is a symlink to a different location, moves from the old target
/// - If `/data/rayhunter` doesn't exist, just creates the symlink
/// - If `/data/rayhunter` is a symlink to `data_dir`, does nothing
pub async fn setup_data_directory<C: DeviceConnection>(conn: &mut C, data_dir: &str) -> Result<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems this only results in a new directory at data_dir if there was old data that needed migrating. should it not do a mkdir -p {data_dir} otherwise?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that's what's on line 139?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there was old data, moving that data to the new location ensures the directory exists

if there was no old data, like 139-140 handles this case.

if data_dir == "/data/rayhunter" {
bail!("data_dir must not be /data/rayhunter");
}

if data_dir.contains("'") {
bail!("data_dir must not contain an apostrophe (')");
}

// Determine where old data lives, if anywhere
let old_data_source = if is_symlink(conn, "/data/rayhunter").await {
let current_target = readlink(conn, "/data/rayhunter").await?;
if current_target == data_dir {
println!("Data directory already configured at {data_dir}");
return Ok(());
}
conn.run_command("rm -f /data/rayhunter").await?;
// The old symlink target is where data actually lives
if dir_exists(conn, &current_target).await {
Some(current_target)
} else {
None
}
} else if dir_exists(conn, "/data/rayhunter").await {
if dir_exists(conn, data_dir).await {
bail!("Both /data/rayhunter and {data_dir} exist and are directories.");
}
// Real directory (pre-migration Orbic state)
Some("/data/rayhunter".to_string())
} else {
None
};

// Migrate old data if present
if let Some(old_source) = &old_data_source {
// Stop rayhunter-daemon so it doesn't write during migration.
// The device will be rebooted at the end of installation anyway.
print!("Stopping rayhunter-daemon ... ");
let _ = conn
.run_command("/etc/init.d/rayhunter_daemon stop 2>/dev/null; true")
.await;
println!("ok");

print!("Migrating data from {old_source} to {data_dir} ... ");

// mv old data into its place. If source and destination are on the same filesystem,
// this is an instant rename.
// XXX: DeviceConnection::run_command does not expose the exit code of the ran command. It
// probably should, or a utility for it should exist?
let mv_output = conn
.run_command(&format!("mv {old_source} '{data_dir}' && echo MV_OK"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single quote old_source

.await?;
if mv_output.contains("MV_OK") {
println!("ok");
} else {
bail!("Failed to move data from {old_source} to {data_dir}:\n{mv_output}");
}
} else {
// No migration needed, just ensure the target directory exists
conn.run_command(&format!("mkdir -p '{data_dir}'")).await?;
}

// Create the symlink
print!("Creating symlink /data/rayhunter -> {data_dir} ... ");
conn.run_command("mkdir -p /data").await?;
conn.run_command(&format!("ln -sf '{data_dir}' /data/rayhunter"))
.await?;
println!("ok");

Ok(())
}

/// Telnet-based connection wrapper
pub struct TelnetConnection {
pub addr: SocketAddr,
Expand Down
43 changes: 41 additions & 2 deletions installer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use env_logger::Env;
use anyhow::bail;

mod connection;
mod moxee;
#[cfg(not(target_os = "android"))]
mod orbic;
mod orbic_auth;
Expand Down Expand Up @@ -40,9 +41,11 @@ 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.
/// Install rayhunter on the Orbic RC400L via network.
#[clap(alias = "orbic-network")]
Orbic(OrbicNetworkArgs),
/// Install rayhunter on the Moxee Hotspot via network.
Moxee(MoxeeArgs),
/// Install rayhunter on the TMobile TMOHS1.
Tmobile(TmobileArgs),
/// Install rayhunter on the Uz801.
Expand Down Expand Up @@ -84,6 +87,12 @@ struct InstallTpLink {
/// Overwrite config.toml even if it already exists on the device.
#[arg(long)]
reset_config: bool,

/// Override the data directory path. Defaults to /cache/rayhunter-data (or SD card path when
/// SD card is used). Must not be /data/rayhunter, which lives on a storage partition that's
/// too small for normal Rayhunter operation.
#[arg(long)]
data_dir: Option<String>,
}

#[derive(Parser, Debug)]
Expand All @@ -110,6 +119,35 @@ struct OrbicNetworkArgs {
/// Overwrite config.toml even if it already exists on the device.
#[arg(long)]
reset_config: bool,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this right? why are we migrating data_dir by default on Orbics?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was easier to make /data/rayhunter a symlink to some other directory in all cases than to special-case the orbic where /data/rayhunter is currently a dir. that's why the layout for orbic would end up with a symlink from /data/rayhunter to /data/rayhunter-data even though it's not really necessary

/// Override the data directory path. Defaults to /data/rayhunter-data.
/// Must not be /data/rayhunter.
#[arg(long)]
data_dir: Option<String>,
}

#[derive(Parser, Debug)]
struct MoxeeArgs {
/// IP address for Moxee admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,

/// Admin username for authentication.
#[arg(long, default_value = "admin")]
admin_username: String,

/// Admin password for authentication.
#[arg(long)]
admin_password: Option<String>,

/// Overwrite config.toml even if it already exists on the device.
#[arg(long)]
reset_config: bool,

/// Override the data directory path. Defaults to /cache/rayhunter-data.
/// Must not be /data/rayhunter.
#[arg(long)]
data_dir: Option<String>,
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -245,7 +283,8 @@ async fn run(args: Args) -> Result<(), Error> {
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
#[cfg(not(target_os = "android"))]
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::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password, args.reset_config, args.data_dir).await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
Command::Moxee(args) => moxee::install(args).await.context("\nFailed to install rayhunter on the Moxee Hotspot")?,
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
Command::Util(subcommand) => {
match subcommand.command {
Expand Down
15 changes: 15 additions & 0 deletions installer/src/moxee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use anyhow::Result;

use crate::MoxeeArgs;

pub async fn install(args: MoxeeArgs) -> Result<()> {
let data_dir = args.data_dir.or(Some("/cache/rayhunter-data".to_string()));
crate::orbic_network::install(
args.admin_ip,
args.admin_username,
args.admin_password,
args.reset_config,
data_dir,
)
.await
}
8 changes: 1 addition & 7 deletions installer/src/orbic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
let mut conn = AdbConnection {
device: &mut adb_device,
};
install_config(
&mut conn,
"/data/rayhunter/config.toml",
"orbic",
reset_config,
)
.await?;
install_config(&mut conn, "orbic", reset_config).await?;
}

install_file(
Expand Down
20 changes: 8 additions & 12 deletions installer/src/orbic_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::Deserialize;
use tokio::time::sleep;

use crate::RAYHUNTER_DAEMON_INIT;
use crate::connection::{TelnetConnection, install_config};
use crate::connection::{TelnetConnection, install_config, setup_data_directory};
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};
Expand Down Expand Up @@ -147,6 +147,7 @@ pub async fn install(
admin_username: String,
admin_password: Option<String>,
reset_config: bool,
data_dir: Option<String>,
) -> Result<()> {
let Some(admin_password) = admin_password else {
eprintln!(
Expand All @@ -170,7 +171,8 @@ pub async fn install(
wait_for_telnet(&admin_ip).await?;
println!("done");

setup_rayhunter(&admin_ip, reset_config).await
let data_dir = data_dir.unwrap_or_else(|| "/data/rayhunter-data".to_string());
setup_rayhunter(&admin_ip, reset_config, &data_dir).await
}

async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
Expand All @@ -194,7 +196,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
Ok(())
}

async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like this should be an optional arg, and we don't call setup_data_directory if it's None

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #886 (comment) -- i've made /data/rayhunter a symlink always, so that the code becomes simpler. orbic is a kind of outlier between these devices as it is the only device where /data/ is the partition where we want rayhunter installed.

let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));

Expand All @@ -208,7 +210,8 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
)
.await?;

telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", false).await?;
let mut conn = TelnetConnection::new(addr, false);
setup_data_directory(&mut conn, data_dir).await?;

telnet_send_file(
addr,
Expand All @@ -218,14 +221,7 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool) -> Result<()> {
)
.await?;

let mut conn = TelnetConnection::new(addr, false);
install_config(
&mut conn,
"/data/rayhunter/config.toml",
"orbic",
reset_config,
)
.await?;
install_config(&mut conn, "orbic", reset_config).await?;

telnet_send_file(
addr,
Expand Down
Loading