use std::{sync::mpsc::Sender, time::Duration}; use anyhow::{bail, Context}; use aprs_parser::{AprsData, AprsPacket}; use base64::{engine::general_purpose, Engine}; use kiss_tnc::Tnc; use sha3::{Digest, Sha3_256}; #[derive(Debug)] enum Command { ReqImageId(u8), BurstBalloon, CutPayload, } impl Command { pub fn decode(msg: &[u8]) -> Option<Self> { match msg.first()? { b'R' => { let id = std::str::from_utf8(msg.get(1..)?).ok()?.parse().ok()?; Some(Self::ReqImageId(id)) } b'B' if msg.len() == 1 => Some(Self::BurstBalloon), b'C' if msg.len() == 1 => Some(Self::CutPayload), _ => None, } } } // handles commands from APRS pub struct CommandHandler { callsign: String, // the minimum id the command must have. Used to prevent replay attacks min_id: u64, // used for the cryptographic signature. Allows us to ignore commands from unknown sources secret: String, burst_command: String, cutdown_command: String, img_request: Sender<u8>, } impl CommandHandler { pub fn new( callsign: String, secret: String, burst_command: String, cutdown_command: String, img_request: Sender<u8>, ) -> Self { Self { callsign, min_id: 0, secret, burst_command, cutdown_command, img_request, } } pub fn process_forever(&self) { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(async { loop { if let Err(e) = self.connect_and_process().await { eprintln!("{}", e); } tokio::time::sleep(Duration::from_secs(1)).await; } }); } async fn connect_and_process(&self) -> anyhow::Result<()> { // TODO don't hardcode this let mut tnc = Tnc::connect_tcp("localhost:8001").await?; let (_, data) = tnc.read_frame().await?; if let Ok(packet) = AprsPacket::decode_ax25(&data) { self.process_packet(&packet); } Ok(()) } fn process_packet(&self, packet: &AprsPacket) { if let AprsData::Message(msg) = &packet.data { if msg.addressee == self.callsign.as_bytes() { if let Err(e) = self.process_msg(&msg.text) { eprintln!("Ignoring APRS packet: {}", e); } } } } fn process_msg(&self, msg: &[u8]) -> anyhow::Result<()> { let msg = String::from_utf8(msg.to_vec())?; let mut iter = msg.split('|'); let cmd = iter.next().context("Missing command")?; let id: u64 = iter .next() .context("Missing id")? .parse() .context("Invalid id")?; let hash_actual = general_purpose::STANDARD .decode(iter.next().context("Missing hash")?) .context("Invalid hash")?; if hash_actual != self.hash(cmd, id) { bail!("Invalid signature"); } if id <= self.min_id { bail!("Given ID too small"); } let cmd = Command::decode(cmd.as_bytes()).context("Invalid command")?; match cmd { Command::ReqImageId(id) => self.img_request.send(id)?, Command::BurstBalloon => { let (_, stderr) = subprocess::Exec::shell(&self.burst_command) .communicate()? .read()?; if let Some(stderr) = stderr { if !stderr.is_empty() { eprintln!("{}", String::from_utf8_lossy(&stderr)); } } } Command::CutPayload => { let (_, stderr) = subprocess::Exec::shell(&self.cutdown_command) .communicate()? .read()?; if let Some(stderr) = stderr { if !stderr.is_empty() { eprintln!("{}", String::from_utf8_lossy(&stderr)); } } } } Ok(()) } fn hash(&self, cmd: &str, id: u64) -> [u8; 16] { let mut hasher = Sha3_256::new(); hasher.update(cmd); hasher.update(b"|"); hasher.update(id.to_be_bytes()); hasher.update(&self.secret); // safe to unwrap because we know 16=16, // even if the compiler doesn't! hasher.finalize()[0..16].try_into().unwrap() } }