Archived
1
This repository has been archived on 2025-02-05. You can view files and clone it, but cannot push or open issues or pull requests.
netbrowse/src/ipc.rs
2021-12-17 13:44:08 -05:00

231 lines
7.5 KiB
Rust

//! 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,
net::IpAddr,
process::{Child, Stdio},
str::FromStr,
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,
#[error(transparent)]
AddrParseError(#[from] std::net::AddrParseError),
#[error(transparent)]
IntParseError(#[from] std::num::ParseIntError),
}
/// 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<char> 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<Self, Self::Error> {
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<String> for IpType {
type Error = PacketParseError;
/// Parses the type of IP address from the avahi-browse output
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.as_str() {
"IPv4" => Ok(IpType::V4),
"IPv6" => Ok(IpType::V6),
_ => Err(PacketParseError::IpTypeParseError(s)),
}
}
}
/// The metadat for a single service
#[derive(Debug)]
pub struct Service {
/// The name of the service
pub name: String,
/// The mDNS hostname of the service
pub hostname: String,
/// The IP address serving the service
pub ip: IpAddr,
/// The port the service is listening on
pub port: u16,
/// All additional data
pub data: Vec<String>,
}
/// 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,
/// The service defined in this packet
pub service: Option<Service>,
}
impl TryFrom<String> for MdnsPacket {
type Error = PacketParseError;
/// Parses a packet from the `avahi-browse` subprocess
fn try_from(s: String) -> Result<Self, Self::Error> {
// Convert the incoming data into an iter over its components
let mut iter = s.split(';');
// Load the data before constructing the structure
// Grabs the first char of the first arg, and reads it as a mode
let 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
let interface_name = try_unwrap_arg(iter.next())?.to_string();
// Grabs the third arg, and reads it as an internet protocol type
let internet_protocol = IpType::try_from(try_unwrap_arg(iter.next())?.to_string())?;
// Grabs the fourth arg, and reads it as a hostname
let hostname = try_unwrap_arg(iter.next())?
.to_string()
.replace("\\.", ".")
.replace("\\0", " ");
// Grabs the fifth arg, and reads it as a service type
let service_type = try_unwrap_arg(iter.next())?.to_string();
// Grabs the sixth arg, and reads it as a domain
let domain = try_unwrap_arg(iter.next())?.to_string();
Ok(Self {
mode,
interface_name,
internet_protocol,
hostname,
service_type: service_type.clone(),
domain,
// Reads the remaining args as service data
// We do this by assuming there is a service, and turning any "no args" errors into a `None` value
service: match (|| -> Result<Service, PacketParseError> {
Ok(Service {
name: service_type,
hostname: try_unwrap_arg(iter.next())?.to_string(),
ip: IpAddr::from_str(try_unwrap_arg(iter.next())?)?,
port: u16::from_str(try_unwrap_arg(iter.next())?)?,
data: iter.map(|s| s.to_string()).collect(),
})
})() {
Ok(s) => Some(s),
Err(e) => {
// eprintln!("{}", e);
None
}
},
})
}
}
/// 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<MdnsPacket>,
}
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::<MdnsPacket>();
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,
}
}
}