diff --git a/Cargo.lock b/Cargo.lock index b96c650..07ed128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1802,6 +1802,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1883,6 +1884,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2455,7 +2457,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.57.0", + "windows-core 0.61.2", ] [[package]] @@ -2711,6 +2713,7 @@ dependencies = [ "bytes", "clap", "env_logger 0.11.8", + "futures", "hyper", "hyper-util", "md5 0.7.0", @@ -2719,6 +2722,7 @@ dependencies = [ "reqwest", "serde", "sha2", + "termios", "tokio", "tokio-retry2", "tokio-stream", @@ -5972,6 +5976,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 9d7fa9a..8b60aac 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -31,6 +31,10 @@ sha2 = "0.10.8" tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-features = false } tokio-retry2 = "0.5.7" tokio-stream = "0.1.17" +futures = "0.3" + +[target.'cfg(unix)'.dependencies] +termios = "0.3" [target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies.adb_client] git = "https://github.com/EFForg/adb_client.git" diff --git a/installer/src/lib.rs b/installer/src/lib.rs index 1186499..37d256d 100644 --- a/installer/src/lib.rs +++ b/installer/src/lib.rs @@ -126,6 +126,8 @@ enum UtilSubCommand { Uz801StartAdb(Uz801Args), /// Root the tplink and launch telnetd. TplinkStartTelnet(TplinkStartTelnet), + /// Root the TP-Link and open an interactive shell. + TplinkShell(TplinkStartTelnet), /// Root the Wingtech and launch telnetd. WingtechStartTelnet(WingtechArgs), /// Root the Wingtech and launch adb. @@ -138,6 +140,8 @@ enum UtilSubCommand { PinephoneStopAdb, /// Root the Orbic and launch telnetd. OrbicStartTelnet(OrbicNetworkArgs), + /// Root the Orbic and open an interactive shell. + OrbicShell(OrbicNetworkArgs), /// Send a file to the TP-Link device over telnet. /// /// Before running this utility, you need to make telnet accessible with `installer util @@ -261,6 +265,9 @@ async fn run(args: Args) -> Result<(), Error> { UtilSubCommand::TplinkStartTelnet(options) => { tplink::start_telnet(&options.admin_ip).await?; } + UtilSubCommand::TplinkShell(options) => { + tplink::shell(&options.admin_ip).await.context("\nFailed to open shell on TP-Link device")?; + } UtilSubCommand::TplinkSendFile(options) => { util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?; } @@ -274,6 +281,7 @@ async fn run(args: Args) -> Result<(), Error> { #[cfg(not(target_os = "android"))] 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, &args.admin_username, args.admin_password.as_deref()).await.context("\\nFailed to start telnet on the Orbic RC400L")?, + UtilSubCommand::OrbicShell(args) => orbic_network::shell(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to open shell on Orbic RC400L")?, } } } diff --git a/installer/src/orbic_network.rs b/installer/src/orbic_network.rs index b66c7b5..638c7e9 100644 --- a/installer/src/orbic_network.rs +++ b/installer/src/orbic_network.rs @@ -9,7 +9,7 @@ use tokio::time::sleep; use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password}; use crate::output::{eprintln, print, println}; -use crate::util::{telnet_send_command, telnet_send_file}; +use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; #[derive(Deserialize, Debug)] @@ -270,3 +270,16 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> { Ok(()) } + +/// Root the Orbic device and open an interactive shell +pub async fn shell( + admin_ip: &str, + admin_username: &str, + admin_password: Option<&str>, +) -> Result<()> { + start_telnet(admin_ip, admin_username, admin_password).await?; + eprintln!( + "This terminal is fairly limited. The shell prompt may not be visible, but it still accepts commands." + ); + interactive_shell(admin_ip, 24, false).await +} diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index a3315ed..0702b7c 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -19,7 +19,7 @@ use tokio::time::sleep; use crate::InstallTpLink; use crate::output::println; -use crate::util::{telnet_send_command, telnet_send_file}; +use crate::util::{interactive_shell, telnet_send_command, telnet_send_file}; type HttpProxyClient = hyper_util::client::legacy::Client; @@ -383,6 +383,12 @@ fn get_rayhunter_daemon(sdcard_path: &str) -> String { ) } +/// Root the TP-Link device and open an interactive shell +pub async fn shell(admin_ip: &str) -> Result<(), Error> { + start_telnet(admin_ip).await?; + interactive_shell(admin_ip, 23, true).await +} + #[test] fn test_get_rayhunter_daemon() { let s = get_rayhunter_daemon("/media/card"); diff --git a/installer/src/util.rs b/installer/src/util.rs index 55b4ba3..c8cbb53 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -11,6 +11,9 @@ use tokio::time::{sleep, timeout}; use crate::output::{print, println}; +#[cfg(unix)] +use std::os::fd::AsRawFd; + pub async fn telnet_send_command_with_output( addr: SocketAddr, command: &str, @@ -225,3 +228,84 @@ pub fn open_usb_device(vid: u16, pid: u16) -> Result> { } Ok(None) } + +/// Open an interactive shell to a device +/// +/// Connects to a shell service on the device and forwards stdin/stdout bidirectionally. +pub async fn interactive_shell(admin_ip: &str, shell_port: u16, raw_mode: bool) -> Result<()> { + let shell_addr = SocketAddr::from_str(&format!("{admin_ip}:{shell_port}"))?; + let mut stream = TcpStream::connect(shell_addr) + .await + .context("Failed to connect to shell. Make sure the device is reachable.")?; + + let stdin = tokio::io::stdin(); + + #[cfg(unix)] + let raw_terminal_guard = if raw_mode { + Some(RawTerminal::new(stdin.as_raw_fd())?) + } else { + None + }; + + // suppress "unused variable" lint + #[cfg(not(unix))] + let _used = raw_mode; + + let mut stdio = tokio::io::join(stdin, tokio::io::stdout()); + let _ = tokio::io::copy_bidirectional(&mut stream, &mut stdio).await; + + // hitting ctrl-d will not print a trailing newline on tplink at least, which messes up the + // next prompt + println!(); + + // The current_thread runtime in tokio will block forever until stdin receives a read error. To + // work around this cleanup issue we just exit directly from here. + // + // This is documented as a flaw in tokio::io::stdin()'s own docs, but the recommended + // workaround to spawn your own OS thread doesn't work. + // + // For some reason this only happens when the terminal is being put in raw mode (removing + // RawTerminal fixes it) + // + // We have to drop the RawTerminal guard before exiting, otherwise we will + // mess up the terminal. + #[cfg(unix)] + drop(raw_terminal_guard); + std::process::exit(0) +} + +#[cfg(unix)] +struct RawTerminal { + fd: std::os::fd::RawFd, + original_termios: termios::Termios, +} + +#[cfg(unix)] +impl RawTerminal { + fn new(fd: std::os::fd::RawFd) -> Result { + // put terminal in raw mode so that arrow keys, tab etc are correctly forwarded to the + // device's shell + let mut original_termios = termios::Termios::from_fd(fd)?; + let raw_termios = original_termios; + + original_termios.c_lflag &= !(termios::ICANON | termios::ECHO | termios::ISIG); + original_termios.c_iflag &= !(termios::IXON | termios::ICRNL); + original_termios.c_oflag &= !termios::OPOST; + original_termios.c_cc[termios::VMIN] = 1; + original_termios.c_cc[termios::VTIME] = 0; + + termios::tcsetattr(fd, termios::TCSANOW, &original_termios)?; + + Ok(RawTerminal { + fd, + original_termios: raw_termios, + }) + } +} + +#[cfg(unix)] +impl Drop for RawTerminal { + fn drop(&mut self) { + let _ = termios::tcsetattr(self.fd, termios::TCSANOW, &self.original_termios); + } +}