mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-04 19:19:09 -07:00
Merge pull request #387 from oopsbagel/wingtech-ct2mhs01
feat: support Wingtech CT2MHS01 hotspot
This commit is contained in:
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
@@ -12,6 +12,7 @@ env:
|
||||
FILE_ROOTSHELL: ../../rootshell/rootshell
|
||||
FILE_RAYHUNTER_DAEMON_ORBIC: ../../rayhunter-daemon-orbic/rayhunter-daemon
|
||||
FILE_RAYHUNTER_DAEMON_TPLINK: ../../rayhunter-daemon-tplink/rayhunter-daemon
|
||||
FILE_RAYHUNTER_DAEMON_WINGTECH: ../../rayhunter-daemon-wingtech/rayhunter-daemon
|
||||
|
||||
jobs:
|
||||
files_changed:
|
||||
@@ -97,8 +98,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- name: tplink
|
||||
- name: orbic
|
||||
- name: tplink
|
||||
- name: wingtech
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -208,8 +210,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- name: tplink
|
||||
- name: orbic
|
||||
- name: tplink
|
||||
- name: wingtech
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
48
Cargo.lock
generated
48
Cargo.lock
generated
@@ -46,6 +46,17 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -348,6 +359,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64_light"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6aca08f76b8485947a20a1b3096e5a8cd6edbcecc6d2a8932df9b41d36aadf"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.7.3"
|
||||
@@ -409,6 +426,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.7.7"
|
||||
@@ -502,6 +528,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
@@ -1429,13 +1465,25 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
"anyhow",
|
||||
"axum",
|
||||
"base64_light",
|
||||
"block-padding",
|
||||
"bytes",
|
||||
"clap",
|
||||
"env_logger 0.11.8",
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
# These feature flags are mutually exclusive, and exactly one must be enabled.
|
||||
orbic = ["rayhunter/orbic"]
|
||||
tplink = ["rayhunter/tplink"]
|
||||
wingtech = ["rayhunter/wingtech"]
|
||||
|
||||
default = ["orbic"]
|
||||
|
||||
|
||||
@@ -15,14 +15,13 @@ mod orbic;
|
||||
#[cfg(feature = "orbic")]
|
||||
pub use orbic::update_ui;
|
||||
|
||||
#[cfg(feature = "wingtech")]
|
||||
mod wingtech;
|
||||
#[cfg(feature = "wingtech")]
|
||||
pub use wingtech::update_ui;
|
||||
|
||||
pub enum DisplayState {
|
||||
Recording,
|
||||
Paused,
|
||||
WarningDetected,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "orbic", feature = "tplink"))]
|
||||
compile_error!("cannot compile for many devices at once");
|
||||
|
||||
#[cfg(not(any(feature = "orbic", feature = "tplink")))]
|
||||
compile_error!("cannot compile for no device at all");
|
||||
|
||||
54
bin/src/display/wingtech.rs
Normal file
54
bin/src/display/wingtech.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
/// Display support for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use crate::config;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use crate::display::DisplayState;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
Dimensions {
|
||||
height: 128,
|
||||
width: 160,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (*b as u16) >> 3;
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
|
||||
std::fs::write(FB_PATH, &raw_buffer).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,11 @@ version = "0.3.4"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
anyhow = "1.0.98"
|
||||
axum = "0.8.3"
|
||||
base64_light = "0.1.5"
|
||||
block-padding = "0.3.3"
|
||||
bytes = "1.10.1"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
|
||||
@@ -8,17 +8,22 @@ fn main() {
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../target/armv7-unknown-linux-musleabihf/firmware/"
|
||||
));
|
||||
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||
set_binary_var(include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||
set_binary_var(
|
||||
&include_dir,
|
||||
include_dir,
|
||||
"FILE_RAYHUNTER_DAEMON_ORBIC",
|
||||
"rayhunter-daemon",
|
||||
);
|
||||
set_binary_var(
|
||||
&include_dir,
|
||||
include_dir,
|
||||
"FILE_RAYHUNTER_DAEMON_TPLINK",
|
||||
"rayhunter-daemon",
|
||||
);
|
||||
set_binary_var(
|
||||
include_dir,
|
||||
"FILE_RAYHUNTER_DAEMON_WINGTECH",
|
||||
"rayhunter-daemon",
|
||||
);
|
||||
}
|
||||
|
||||
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
@@ -26,7 +31,7 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
let blank = Path::new(&out_dir).join("blank");
|
||||
std::fs::write(&blank, &[]).unwrap();
|
||||
std::fs::write(&blank, []).unwrap();
|
||||
println!("cargo::rustc-env={var}={}", blank.display());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use env_logger::Env;
|
||||
|
||||
mod orbic;
|
||||
mod tplink;
|
||||
mod util;
|
||||
mod wingtech;
|
||||
|
||||
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example");
|
||||
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
||||
@@ -21,6 +23,8 @@ enum Command {
|
||||
Orbic(InstallOrbic),
|
||||
/// Install rayhunter on the TP-Link M7350.
|
||||
Tplink(InstallTpLink),
|
||||
/// Install rayhunter on the Wingtech CT2MHS01.
|
||||
Wingtech(WingtechArgs),
|
||||
/// Developer utilities.
|
||||
Util(Util),
|
||||
}
|
||||
@@ -65,6 +69,10 @@ enum UtilSubCommand {
|
||||
Shell(Shell),
|
||||
/// Root the tplink and launch telnetd.
|
||||
TplinkStartTelnet(TplinkStartTelnet),
|
||||
/// Root the Wingtech and launch telnetd.
|
||||
WingtechStartTelnet(WingtechArgs),
|
||||
/// Root the Wingtech and launch adb.
|
||||
WingtechStartAdb(WingtechArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -74,6 +82,17 @@ struct TplinkStartTelnet {
|
||||
admin_ip: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct WingtechArgs {
|
||||
/// IP address for Wingtech admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Serial {
|
||||
#[arg(long)]
|
||||
@@ -91,6 +110,7 @@ async fn run() -> Result<(), Error> {
|
||||
match command {
|
||||
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.")?,
|
||||
Command::Orbic(_) => orbic::install().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 {
|
||||
UtilSubCommand::Serial(serial_cmd) => {
|
||||
if serial_cmd.root {
|
||||
@@ -114,6 +134,8 @@ async fn run() -> Result<(), Error> {
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
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")?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use nusb::{Device, Interface};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::util::echo;
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||
@@ -40,13 +41,6 @@ const RNDIS_INTERFACE: u8 = 0;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const RNDIS_INTERFACE: u8 = 1;
|
||||
|
||||
macro_rules! echo {
|
||||
($($arg:tt)*) => {
|
||||
print!($($arg)*);
|
||||
let _ = std::io::stdout().flush();
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
let mut adb_device = force_debug_mode().await?;
|
||||
echo!("Installing rootshell... ");
|
||||
|
||||
@@ -15,11 +15,10 @@ use bytes::{Bytes, BytesMut};
|
||||
use hyper::StatusCode;
|
||||
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
|
||||
use serde::Deserialize;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, timeout};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::InstallTpLink;
|
||||
use crate::util::{telnet_send_command, telnet_send_file};
|
||||
|
||||
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
||||
|
||||
@@ -164,6 +163,7 @@ async fn tplink_run_install(
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
@@ -200,99 +200,6 @@ async fn tplink_run_install(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<(), Error> {
|
||||
println!("Sending file {filename}");
|
||||
|
||||
// remove the old file just in case we are close to disk capacity.
|
||||
telnet_send_command(addr, &format!("rm {filename}"), "").await?;
|
||||
|
||||
{
|
||||
let filename = filename.to_owned();
|
||||
let handle = tokio::spawn(async move {
|
||||
telnet_send_command(addr, &format!("nc -l 0.0.0.0:8081 > {filename}.tmp"), "").await
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let mut addr = addr;
|
||||
addr.set_port(8081);
|
||||
let mut stream = TcpStream::connect(addr).await?;
|
||||
stream.write_all(payload).await?;
|
||||
|
||||
handle.await??;
|
||||
}
|
||||
|
||||
let checksum = md5::compute(payload);
|
||||
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("md5sum {filename}.tmp"),
|
||||
&format!("{checksum:x} {filename}.tmp"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("mv {filename}.tmp {filename}"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn telnet_send_command(
|
||||
addr: SocketAddr,
|
||||
command: &str,
|
||||
expected_output: &str,
|
||||
) -> Result<(), Error> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
|
||||
loop {
|
||||
let mut next_byte = 0;
|
||||
reader
|
||||
.read_exact(std::slice::from_mut(&mut next_byte))
|
||||
.await?;
|
||||
if next_byte == b'#' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_all(command.as_bytes()).await?;
|
||||
writer.write_all(b"; echo exit code $?\r\n").await?;
|
||||
|
||||
let mut read_buf = Vec::new();
|
||||
|
||||
let _ = timeout(Duration::from_secs(5), async {
|
||||
let mut buf = [0; 4096];
|
||||
loop {
|
||||
let Ok(bytes_read) = reader.read(&mut buf).await else {
|
||||
break;
|
||||
};
|
||||
let bytes = &buf[..bytes_read];
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
read_buf.extend(bytes);
|
||||
|
||||
if read_buf.ends_with(b"/ # ") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let string = String::from_utf8_lossy(&read_buf);
|
||||
|
||||
if !string.contains(expected_output) {
|
||||
anyhow::bail!("{expected_output:?} not found in: {string}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
client: HttpProxyClient,
|
||||
|
||||
90
installer/src/util.rs
Normal file
90
installer/src/util.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
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;
|
||||
|
||||
pub async fn telnet_send_command(
|
||||
addr: SocketAddr,
|
||||
command: &str,
|
||||
expected_output: &str,
|
||||
) -> Result<()> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
loop {
|
||||
let mut next_byte = 0;
|
||||
reader
|
||||
.read_exact(std::slice::from_mut(&mut next_byte))
|
||||
.await?;
|
||||
if next_byte == b'#' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
writer.write_all(command.as_bytes()).await?;
|
||||
writer.write_all(b"; echo exit code $?\r\n").await?;
|
||||
let mut read_buf = Vec::new();
|
||||
let _ = timeout(Duration::from_secs(5), async {
|
||||
let mut buf = [0; 4096];
|
||||
loop {
|
||||
let Ok(bytes_read) = reader.read(&mut buf).await else {
|
||||
break;
|
||||
};
|
||||
let bytes = &buf[..bytes_read];
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
read_buf.extend(bytes);
|
||||
if read_buf.ends_with(b"/ # ") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let string = String::from_utf8_lossy(&read_buf);
|
||||
if !string.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {string}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<()> {
|
||||
echo!("Sending file {filename} ... ");
|
||||
{
|
||||
let filename = filename.to_owned();
|
||||
let handle = tokio::spawn(async move {
|
||||
telnet_send_command(addr, &format!("nc -l -p 8081 >{filename}.tmp"), "").await
|
||||
});
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
let mut addr = addr;
|
||||
addr.set_port(8081);
|
||||
let mut stream = TcpStream::connect(addr).await?;
|
||||
stream.write_all(payload).await?;
|
||||
handle.await??;
|
||||
}
|
||||
let checksum = md5::compute(payload);
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("md5sum {filename}.tmp"),
|
||||
&format!("{checksum:x} {filename}.tmp"),
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("mv {filename}.tmp {filename}"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
println!("ok");
|
||||
Ok(())
|
||||
}
|
||||
182
installer/src/wingtech.rs
Normal file
182
installer/src/wingtech.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
/// Installer for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
/// 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;
|
||||
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use base64_light::base64_encode_bytes;
|
||||
use block_padding::{Padding, Pkcs7};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::WingtechArgs as Args;
|
||||
use crate::util::{echo, telnet_send_command, telnet_send_file};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn install(
|
||||
Args {
|
||||
admin_ip,
|
||||
admin_password,
|
||||
}: Args,
|
||||
) -> Result<()> {
|
||||
wingtech_run_install(admin_ip, admin_password).await
|
||||
}
|
||||
|
||||
const KEY: &[u8] = b"abcdefghijklmn12";
|
||||
|
||||
/// Returns password encrypted in AES128 ECB mode with the key b"abcdefghijklmn12",
|
||||
/// with Pkcs7 padding, encoded in base64.
|
||||
fn encrypt_password(password: &[u8]) -> Result<String> {
|
||||
let c = Aes128::new_from_slice(KEY)?;
|
||||
let mut b = GenericArray::from([0u8; 16]);
|
||||
b[..password.len()].copy_from_slice(password);
|
||||
Pkcs7::pad(&mut b, password.len());
|
||||
c.encrypt_block(&mut b);
|
||||
Ok(base64_encode_bytes(&b))
|
||||
}
|
||||
|
||||
pub async fn start_telnet(admin_ip: &str, admin_password: &str) -> Result<()> {
|
||||
run_command(admin_ip, admin_password, "busybox telnetd -l /bin/sh").await
|
||||
}
|
||||
|
||||
pub async fn start_adb(admin_ip: &str, admin_password: &str) -> Result<()> {
|
||||
run_command(admin_ip, admin_password, "/sbin/usb/compositions/9025").await
|
||||
}
|
||||
|
||||
async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Result<()> {
|
||||
let qcmap_auth_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_auth");
|
||||
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
|
||||
|
||||
let encrypted_pw = encrypt_password(admin_password.as_bytes()).ok().unwrap();
|
||||
|
||||
let client = Client::new();
|
||||
let LoginResponse { token } = client
|
||||
.post(&qcmap_auth_endpoint)
|
||||
.body(format!(
|
||||
"type=login&pwd={encrypted_pw}&timeout=60000&user=admin"
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.context("login did not return a token in response")?;
|
||||
|
||||
let command = client.post(&qcmap_web_cgi_endpoint)
|
||||
.body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
|
||||
.send()
|
||||
.await?;
|
||||
if command.status() != 200 {
|
||||
bail!(
|
||||
"running command failed with status code: {:?}",
|
||||
command.status()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
||||
echo!("Starting telnet ... ");
|
||||
start_telnet(&admin_ip, &admin_password).await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("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").await?;
|
||||
println!("ok");
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/config.toml",
|
||||
crate::CONFIG_TOML.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_WINGTECH"));
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"chmod 755 /data/rayhunter/rayhunter-daemon",
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
crate::RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"chmod 755 /etc/init.d/rayhunter_daemon",
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
telnet_send_command(addr, "reboot", "exit code 0").await?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
echo!("Testing rayhunter ... ");
|
||||
let max_failures = 10;
|
||||
http_ok_every(
|
||||
format!("http://{admin_ip}:8080/index.html"),
|
||||
Duration::from_secs(3),
|
||||
max_failures,
|
||||
)
|
||||
.await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running at http://{admin_ip}:8080");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn http_ok_every(rayhunter_url: String, interval: Duration, max_failures: u32) -> Result<()> {
|
||||
let client = Client::new();
|
||||
let mut failures = 0;
|
||||
loop {
|
||||
match client.get(&rayhunter_url).send().await {
|
||||
Ok(test) => match test.status().is_success() {
|
||||
true => break,
|
||||
false => bail!(
|
||||
"request for url ({rayhunter_url}) failed with status code: {:?}",
|
||||
test.status()
|
||||
),
|
||||
},
|
||||
Err(e) => match failures > max_failures {
|
||||
true => return Err(e.into()),
|
||||
false => failures += 1,
|
||||
},
|
||||
}
|
||||
sleep(interval).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_password() {
|
||||
let p = b"80536913";
|
||||
let s = encrypt_password(p).ok();
|
||||
let expected = Some("5brvd8xl732cSoFTAy67ig==".to_string());
|
||||
assert_eq!(s, expected);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ path = "src/lib.rs"
|
||||
default = []
|
||||
orbic = []
|
||||
tplink = []
|
||||
wingtech = []
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.5.0"
|
||||
|
||||
Reference in New Issue
Block a user