diff --git a/installer/src/main.rs b/installer/src/main.rs index 392ac5d..5e93001 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -1,14 +1,10 @@ -use std::net::SocketAddr; -use std::str::FromStr; -use std::time::Duration; - use anyhow::{Context, Error}; use clap::{Parser, Subcommand}; -use serde::Deserialize; -use serde_json::json; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio::time::{sleep, timeout}; + +mod tplink; + +pub static CONFIG_TOML: &[u8] = include_bytes!("../../dist/config.toml.example"); +pub static RAYHUNTER_DAEMON_INIT: &[u8] = include_bytes!("../../dist/scripts/rayhunter_daemon"); #[derive(Parser, Debug)] #[command(version, about)] @@ -48,225 +44,7 @@ async fn main() -> Result<(), Error> { let Args { command } = Args::parse(); match command { - Command::InstallTplink(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. Currently only Hardware Revision v3 is supported.")?, - } - - Ok(()) -} - -async fn main_tplink(args: InstallTpLink) -> Result<(), Error> { - let InstallTpLink { - skip_sdcard, - username, - password, - admin_ip, - } = args; - - 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"); - - #[derive(Deserialize)] - struct NonceResponse { - nonce: String, - } - - let client = reqwest::Client::new(); - - let NonceResponse { nonce } = client - .post(&qcmap_auth_endpoint) - .body(r#"{"module":"authenticator","action":0}"#) - .send() - .await? - .error_for_status()? - .json() - .await?; - - println!("Successfully detected TP-Link M7350 v3, nonce: {nonce}"); - - let digest_md5 = md5::compute(format!("{username}:{password}:{nonce}").as_bytes()); - - #[derive(Deserialize)] - struct TokenResponse { - token: String, - } - - let TokenResponse { token } = client - .post(&qcmap_auth_endpoint) - .json(&json!({ - "module": "authenticator", - "action": 1, - "digest": format!("{:x}", digest_md5) - })) - .send() - .await? - .error_for_status()? - .json() - .await?; - - println!("Got token: {token}"); - - for language in &["$(busybox telnetd -l /bin/sh)", "en"] { - println!("Setting language of device to {language}"); - client - .post(&qcmap_web_cgi_endpoint) - .header("Cookie", format!("tpweb_token={token}")) - .json(&json!({ - "token": token, - "module": "webServer", - "action": 1, - "language": language - })) - .send() - .await? - .error_for_status()?; - } - - println!("Connecting via telnet to {admin_ip}"); - let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); - - if !skip_sdcard { - println!("Mounting sdcard"); - telnet_send_command(addr, "mount /dev/mmcblk0p1 /mnt/card", "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?; - } - - // there is too little space on the internal flash to store anything, but the initrd script - // expects things to be at this location - telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?; - telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?; - telnet_send_command(addr, "ln -sf /mnt/card /data/rayhunter", "exit code 0").await?; - - telnet_send_file( - addr, - "/mnt/card/config.toml", - include_bytes!("../../dist/config.toml.example"), - ) - .await?; - - #[cfg(feature = "vendor")] - let rayhunter_daemon_bin = - include_bytes!("../../rayhunter-daemon-tplink/rayhunter-daemon"); - - #[cfg(not(feature = "vendor"))] - let rayhunter_daemon_bin = - &tokio::fs::read("target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon").await?; - - telnet_send_file(addr, "/mnt/card/rayhunter-daemon", rayhunter_daemon_bin).await?; - telnet_send_file( - addr, - "/etc/init.d/rayhunter_daemon", - include_bytes!("../../dist/scripts/rayhunter_daemon"), - ) - .await?; - - telnet_send_command( - addr, - "chmod ugo+x /mnt/card/rayhunter-daemon", - "exit code 0", - ) - .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!( - "Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080" - ); - - telnet_send_command(addr, "reboot", "exit code 0").await?; - - 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 buf = [0]; - reader.read(&mut buf).await?; - if buf[0] == 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}"); + Command::InstallTplink(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. Currently only Hardware Revision v3 is supported.")?, } Ok(()) diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs new file mode 100644 index 0000000..fd5bc48 --- /dev/null +++ b/installer/src/tplink.rs @@ -0,0 +1,224 @@ +use std::net::SocketAddr; +use std::str::FromStr; +use std::time::Duration; + +use anyhow::{Context, Error}; +use serde::Deserialize; +use serde_json::json; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::time::{sleep, timeout}; + +use crate::InstallTpLink; + +pub async fn main_tplink(args: InstallTpLink) -> Result<(), Error> { + let InstallTpLink { + skip_sdcard, + username, + password, + admin_ip, + } = args; + + 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"); + + #[derive(Deserialize)] + struct NonceResponse { + nonce: String, + } + + let client = reqwest::Client::new(); + + let NonceResponse { nonce } = client + .post(&qcmap_auth_endpoint) + .body(r#"{"module":"authenticator","action":0}"#) + .send() + .await? + .error_for_status()? + .json() + .await?; + + println!("Successfully detected TP-Link M7350 v3, nonce: {nonce}"); + + let digest_md5 = md5::compute(format!("{username}:{password}:{nonce}").as_bytes()); + + #[derive(Deserialize)] + struct TokenResponse { + token: String, + } + + let TokenResponse { token } = client + .post(&qcmap_auth_endpoint) + .json(&json!({ + "module": "authenticator", + "action": 1, + "digest": format!("{:x}", digest_md5) + })) + .send() + .await? + .error_for_status()? + .json() + .await?; + + println!("Got token: {token}"); + + for language in &["$(busybox telnetd -l /bin/sh)", "en"] { + println!("Setting language of device to {language}"); + client + .post(&qcmap_web_cgi_endpoint) + .header("Cookie", format!("tpweb_token={token}")) + .json(&json!({ + "token": token, + "module": "webServer", + "action": 1, + "language": language + })) + .send() + .await? + .error_for_status()?; + } + + println!("Connecting via telnet to {admin_ip}"); + let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); + + if !skip_sdcard { + println!("Mounting sdcard"); + telnet_send_command(addr, "mount /dev/mmcblk0p1 /mnt/card", "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?; + } + + // there is too little space on the internal flash to store anything, but the initrd script + // expects things to be at this location + telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?; + telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?; + telnet_send_command(addr, "ln -sf /mnt/card /data/rayhunter", "exit code 0").await?; + + telnet_send_file(addr, "/mnt/card/config.toml", crate::CONFIG_TOML).await?; + + #[cfg(feature = "vendor")] + let rayhunter_daemon_bin = include_bytes!("../../rayhunter-daemon-tplink/rayhunter-daemon"); + + #[cfg(not(feature = "vendor"))] + let rayhunter_daemon_bin = + &tokio::fs::read("target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon").await?; + + telnet_send_file(addr, "/mnt/card/rayhunter-daemon", rayhunter_daemon_bin).await?; + telnet_send_file( + addr, + "/etc/init.d/rayhunter_daemon", + crate::RAYHUNTER_DAEMON_INIT, + ) + .await?; + + telnet_send_command( + addr, + "chmod ugo+x /mnt/card/rayhunter-daemon", + "exit code 0", + ) + .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!( + "Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080" + ); + + telnet_send_command(addr, "reboot", "exit code 0").await?; + + 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 buf = [0]; + reader.read(&mut buf).await?; + if buf[0] == 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(()) +}