From 309e463578114e38c8f95ea5265ddff874bb8172 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 17 Dec 2021 12:27:37 -0500 Subject: [PATCH] Working packet parser --- Cargo.toml | 15 ++-- README.md | 14 +-- src/ipc.rs | 183 ++++++++++++++++++++++++++++++++++++++++ src/{lib.rs => main.rs} | 19 ++++- 4 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 src/ipc.rs rename src/{lib.rs => main.rs} (84%) diff --git a/Cargo.toml b/Cargo.toml index 62ade99..e2db741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "" +name = "netbrowse" version = "0.1.0" authors = ["Evan Pratten "] edition = "2018" -description = "" -documentation = "https://docs.rs/" +description = "A graphical frontend to avahi-browse" +documentation = "https://docs.rs/netbrowse" readme = "README.md" -homepage = "https://github.com/ewpratten/" -repository = "https://github.com/ewpratten/" +homepage = "https://github.com/ewpratten/netbrowse" +repository = "https://github.com/ewpratten/netbrowse" license = "GPL-3.0" keywords = [] categories = [] @@ -15,3 +15,8 @@ categories = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +which = "4.2.2" +colored = "2.0.0" +eframe = "0.15.0" +ipnet = "2.3.1" +thiserror = "1.0.30" \ No newline at end of file diff --git a/README.md b/README.md index ee77e0d..9d5bc27 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# -[![Crates.io](https://img.shields.io/crates/v/)](https://crates.io/crates/) -[![Docs.rs](https://docs.rs//badge.svg)](https://docs.rs/) -[![Build](https://github.com/Ewpratten//actions/workflows/build.yml/badge.svg)](https://github.com/Ewpratten//actions/workflows/build.yml) -[![Clippy](https://github.com/Ewpratten//actions/workflows/clippy.yml/badge.svg)](https://github.com/Ewpratten//actions/workflows/clippy.yml) -[![Audit](https://github.com/Ewpratten//actions/workflows/audit.yml/badge.svg)](https://github.com/Ewpratten//actions/workflows/audit.yml) +# netbrowse +[![Crates.io](https://img.shields.io/crates/v/netbrowse)](https://crates.io/crates/netbrowse) +[![Docs.rs](https://docs.rs/netbrowse/badge.svg)](https://docs.rs/netbrowse) +[![Build](https://github.com/Ewpratten/netbrowse/actions/workflows/build.yml/badge.svg)](https://github.com/Ewpratten/netbrowse/actions/workflows/build.yml) +[![Clippy](https://github.com/Ewpratten/netbrowse/actions/workflows/clippy.yml/badge.svg)](https://github.com/Ewpratten/netbrowse/actions/workflows/clippy.yml) +[![Audit](https://github.com/Ewpratten/netbrowse/actions/workflows/audit.yml/badge.svg)](https://github.com/Ewpratten/netbrowse/actions/workflows/audit.yml) repo description @@ -13,5 +13,5 @@ repo description This crate can be installed via `cargo` with: ```sh -cargo install +cargo install netbrowse ``` diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 0000000..e6f580b --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,183 @@ +//! This module handles pulling and parsing data from an `avahi-browse` subprocess in a way that the GUI can render + +use std::{ + convert::TryFrom, + io::BufRead, + process::{Child, Stdio}, + sync::mpsc::{self, Receiver, Sender}, + thread::{self, JoinHandle}, +}; + +use thiserror::Error; + +/// Defines possible errors generated by the packet parsing process +#[derive(Debug, Error)] +pub enum PacketParseError { + #[error("Failed to parse mode char: {0}")] + ModeParseError(char), + #[error("Failed to internet protocol type: {0}")] + IpTypeParseError(String), + #[error("Ran out of arguments")] + NotEnoughArgsError, +} + +/// Tries to unwrap a string from an arg iter, or just returns an error +fn try_unwrap_arg(arg: Option<&str>) -> Result<&str, PacketParseError> { + arg.ok_or(PacketParseError::NotEnoughArgsError) +} + +/// Defines the type of packet received from the `avahi-browse` subprocess +#[derive(Debug)] +pub enum PacketMode { + /// A new service was discovered + New, + /// An existing service was updated + Update, + /// An existing service was removed + Remove, +} + +impl TryFrom for PacketMode { + type Error = PacketParseError; + + /// Parses one of avahi's update chars into something we can make use of + fn try_from(c: char) -> Result { + match c { + '+' => Ok(PacketMode::New), + '=' => Ok(PacketMode::Update), + '-' => Ok(PacketMode::Remove), + _ => Err(PacketParseError::ModeParseError(c)), + } + } +} + +/// Used to specify weather a packet came over IPv4 or IPv6 +#[derive(Debug)] +pub enum IpType { + V4, + V6, +} + +impl TryFrom for IpType { + type Error = PacketParseError; + + /// Parses the type of IP address from the avahi-browse output + fn try_from(s: String) -> Result { + match s.as_str() { + "IPv4" => Ok(IpType::V4), + "IPv6" => Ok(IpType::V6), + _ => Err(PacketParseError::IpTypeParseError(s)), + } + } +} + +/// An information packet parsed from a `avahi-browse` subprocess +#[derive(Debug)] +pub struct MdnsPacket { + /// The type of packet received + pub mode: PacketMode, + /// The interface the packet was received on + pub interface_name: String, + /// The internet protocol type of the packet + pub internet_protocol: IpType, + /// The hostname / service name + pub hostname: String, + /// The type of the service + pub service_type: String, + /// The domain of the service + pub domain: String, + /// Additional service metadata + pub metadata: Vec, +} + +impl TryFrom for MdnsPacket { + type Error = PacketParseError; + + /// Parses a packet from the `avahi-browse` subprocess + fn try_from(s: String) -> Result { + // Convert the incoming data into an iter over its components + let mut iter = s.split(';'); + + Ok(Self { + // Grabs the first char of the first arg, and reads it as a mode + mode: PacketMode::try_from( + try_unwrap_arg(iter.next())? + .chars() + .next() + .ok_or(PacketParseError::NotEnoughArgsError)?, + )?, + // Grabs the second arg, and reads it as an interface name + interface_name: try_unwrap_arg(iter.next())?.to_string(), + // Grabs the third arg, and reads it as an internet protocol type + internet_protocol: IpType::try_from(try_unwrap_arg(iter.next())?.to_string())?, + // Grabs the fourth arg, and reads it as a hostname + hostname: try_unwrap_arg(iter.next())?.to_string(), + // Grabs the fifth arg, and reads it as a service type + service_type: try_unwrap_arg(iter.next())?.to_string(), + // Grabs the sixth arg, and reads it as a domain + domain: try_unwrap_arg(iter.next())?.to_string(), + // Grabs the remaining args, and reads them as metadata + metadata: iter.map(|s| s.to_string()).collect(), + }) + } +} + +/// Encapsulates everything needed to read from and control an `avahi-browse` subprocess +#[derive(Debug)] +pub struct AvahiSubprocess { + thread: JoinHandle<()>, + + /// This stream sends parsed packets back to the main thread + pub packet_stream: Receiver, +} + +impl AvahiSubprocess { + /// Creates a new `avahi-browse` subprocess + pub fn spawn() -> Self { + // Create a channel to send parsed packets back to the main thread + let (packet_sender, packet_receiver) = mpsc::channel::(); + + let thread = thread::spawn(move || { + // Spawn avahi-browse + let child = std::process::Command::new("avahi-browse") + .arg("-p") + .arg("-r") + .arg("-a") + .stdout(Stdio::piped()) + .spawn() + .expect("Failed to spawn avahi-browse subprocess"); + + // Create a stream from the stdout of the child + let stream = child + .stdout + .expect("Failed to get stdout of avahi-browse subprocess"); + let stream = std::io::BufReader::new(stream); + let stream = stream.lines(); + + // Parse the lines from the stream + for line in stream { + let line = line.expect("Failed to read line from avahi-browse subprocess"); + + match MdnsPacket::try_from(line.to_string()) { + Ok(packet) => { + // Send the packet to the main thread + println!("{:?}", packet); + packet_sender + .send(packet) + .expect("Failed to send packet to main thread"); + } + Err(e) => { + // Log the error + eprintln!("Failed to parse packet: {}", e); + } + } + } + }); + + // Return the subprocess + Self { + thread, + packet_stream: packet_receiver, + } + } +} diff --git a/src/lib.rs b/src/main.rs similarity index 84% rename from src/lib.rs rename to src/main.rs index f1b9f47..1dc722f 100644 --- a/src/lib.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -#![doc = include_str!("../README.md")] #![deny(unsafe_code)] #![warn( clippy::all, @@ -64,3 +63,21 @@ nonstandard_style, rust_2018_idioms )] + +use colored::Colorize; +mod ipc; + +fn main() { + + // Check for avahi-browse on this system + if let Err(_) = which::which("avahi-browse") { + eprintln!("{}", "`avahi-browse` was not found in system $PATH".red()); + std::process::exit(1); + } + + // Spawn the subprocess + let subprocess = ipc::AvahiSubprocess::spawn(); + + loop{} + +} \ No newline at end of file