From 44e63e679e86c9e269496e6299430bf6cb684a95 Mon Sep 17 00:00:00 2001 From: Imbus Date: Tue, 20 Jul 2021 16:16:50 +0200 Subject: [PATCH] Cli implemented, somewhat complete functionality --- .gitignore | 1 + README.md | 11 + src/cli_args.rs | 70 +++++- src/host.rs | 20 +- src/machines.rs | 63 ++++-- src/main.rs | 498 +++++++++++++++++++++++++++++++++++++----- src/packet.rs | 7 + src/random_machine.rs | 22 +- src/sanitizers.rs | 73 +++++++ waker.json | 31 ++- 10 files changed, 696 insertions(+), 100 deletions(-) create mode 100644 src/sanitizers.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..a37db36 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +waker.json diff --git a/README.md b/README.md index f628d1d..700413d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,18 @@ TODO: Review parsing methods +Add confirm for wake all Hosts should be pinged on listing +Structs defined: + Host + - Implement methods to retrieve as macbytes/ipv4addr + - Field containing MagicPacket? + Machines + - WakeAll method + - PingAll method + MagicPacket + - Finalze parsers + The format of a Wake-on-LAN (WOL) magic packet is defined as a byte array with 6 bytes of value 255 (0xFF) and 16 repetitions of the target machine’s 48-bit (6-byte) MAC address. diff --git a/src/cli_args.rs b/src/cli_args.rs index c6f0f48..e19961b 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -1,3 +1,6 @@ +// use std::{path::PathBuf, str::FromStr}; + +use crate::{BackupMode, RunMode, WakeMode}; use clap::{App, Arg, ArgMatches}; // use crate::main::RunMode; @@ -11,6 +14,32 @@ use clap::{App, Arg, ArgMatches}; // waker -e, --edit // Enters interactive editing mode // waker --backup-config [file] // Prints to stdout unless file is specified +// This is essentially and abstraction of clap +/// Parses command line arguments and returns a RunMode enum containing desired run mode. +pub fn get_runmode() -> RunMode { + let matches = get_cli_matches(); + if matches.is_present("add") { + return RunMode::Add; + } + if matches.is_present("all") { + return RunMode::Wake(WakeMode::WakeAll); + } + if matches.is_present("edit") { + return RunMode::Edit; + } + if matches.is_present("list") { + return RunMode::List; + } + if matches.is_present("backup") { + let path_str = matches.value_of("backup").unwrap(); + return RunMode::Backup(BackupMode::ToFile(path_str.to_string())); + } + if matches.is_present("print_config") { + return RunMode::Backup(BackupMode::ToStdout); + } + return RunMode::Wake(WakeMode::WakeSome); +} + pub fn get_cli_matches() -> ArgMatches<'static> { /* Move this out to a function that returns a config struct with all the * options */ @@ -21,29 +50,48 @@ pub fn get_cli_matches() -> ArgMatches<'static> { .author("Imbus64") .about("Utility for sending magic packets to configured machines.") .arg( - Arg::with_name("all") + Arg::with_name("add") .short("a") + .long("add") + .help("Add a new host"), + ) + .arg( + Arg::with_name("all") .long("all") .help("Wake all configured hosts"), ) + .arg( + Arg::with_name("edit") + .short("e") + .long("edit") + .help("Enter edit mode"), + ) .arg( Arg::with_name("list") .short("l") .long("list") - .conflicts_with("weight") - .help("Print all entries"), + .help("List all configured entries"), ) .arg( - Arg::with_name("raw") - .long("raw") - .conflicts_with_all(&["list", "plain"]) - .help("Print raw log file to stdout"), + Arg::with_name("backup") + .long("backup") + .conflicts_with_all(&["list", "all"]) + .help("Backup configuration file") + .value_name("File"), ) .arg( - Arg::with_name("plain") - .long("plain") - .conflicts_with_all(&["list", "raw"]) - .help("Print all entries without pretty table formatting"), + Arg::with_name("print_config") + .long("print-config") + .short("p") + .conflicts_with_all(&["list", "all"]) + .help("Print contents of configuration file to stdout"), ) + .arg( + Arg::with_name("MAC ADDRESSES") + .conflicts_with_all(&["all", "list", "edit", "backup"]) + .multiple(true), + ) + // .short("MAC to be directly woken") + // .long("asdf") .get_matches(); } diff --git a/src/host.rs b/src/host.rs index 3356afc..5c5215e 100644 --- a/src/host.rs +++ b/src/host.rs @@ -1,10 +1,12 @@ use serde::{Deserialize, Serialize}; +use crate::packet::MagicPacket; + #[derive(Serialize, Deserialize)] pub struct Host { pub name: String, pub macs: Vec, - // pub ips: Vec, + pub ips: Vec, } impl Host { @@ -12,7 +14,21 @@ impl Host { Host { name: name.into(), macs: vec![mac.into()], - // ips: vec![ipv4], + ips: vec![ipv4.into()], + } + } + + pub fn wake(&self) { + for mac_str in &self.macs { + MagicPacket::from_str(&mac_str).unwrap().send().unwrap(); } } } + +impl std::fmt::Display for Host { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let macs_str = format!("{:?}", &self.macs); + let ips_str = format!("{:?}", &self.ips); + write!(f, "{:<16} {} - {}", self.name, macs_str, ips_str) + } +} diff --git a/src/machines.rs b/src/machines.rs index 3f9435b..cfbb969 100644 --- a/src/machines.rs +++ b/src/machines.rs @@ -7,6 +7,7 @@ use std::{ }; use crate::host::Host; +use crate::packet::MagicPacket; use serde::{Deserialize, Serialize}; // Possibly rename to HostList @@ -22,30 +23,45 @@ impl Machines { } } - pub fn add(&mut self, name: &str, mac_addr: &str) { - self.list.push(Host { - name: name.to_string(), - macs: vec![mac_addr.to_string()], - }); + // This one needs refactoring... + /// Add new host to the list, taking a name and a mac, with an optional IP-adress + pub fn add(&mut self, name: &str, mac_addr: &str, ip_addr: Option) { + match ip_addr { + Some(ip_addr) => { + self.list.push(Host::new(name.to_string(), mac_addr.to_string(), ip_addr.to_string())) + } + None => { + self.list.push(Host { + name: name.to_string(), + macs: vec![mac_addr.to_string()], + ips: vec![], + }); + } + } } + /// Parses the Machine object from a json file pub fn from_json_file(json_path: &PathBuf) -> Result> { let machines: Machines; if json_path.exists() || json_path.is_file() { let json: String = std::fs::read_to_string(&json_path)?.parse()?; machines = serde_json::from_str(&json)?; - // println!("Machines loaded from json"); } else { machines = Machines::new(); let serialized = serde_json::to_string_pretty(&machines)?; let mut file = File::create(&json_path)?; file.write_all(&serialized.as_bytes())?; std::fs::write(&json_path, &serialized)?; - // println!("Machines created"); } Ok(machines) } + fn create_skeleton_config(file: &PathBuf) -> Result<(), Box> { + let skel_machines = Machines::new(); + skel_machines.dump(file)?; + return Ok(()); + } + /// Dump this struct in json format. Will NOT create file. pub fn dump(&self, json_path: &PathBuf) -> Result> { let serialized = serde_json::to_string_pretty(&self)?; @@ -54,10 +70,30 @@ impl Machines { .truncate(true) .open(&json_path)?; file.write_all(&serialized.as_bytes())?; - // println!("Object written to existing file (truncated)"); - // println!("{}", json_path.to_str().unwrap()); Ok(true) } + + /// Attempts to wake all configured hosts via the default os-provided network interface + pub fn wakeall(&self) { + for host in &self.list { + for mac_str in &host.macs { + MagicPacket::from_str(mac_str).unwrap().send().unwrap(); + } + } + } +} + +// I would like to have some kind of iterator comparison here (for the newline), for now this will do... +impl std::fmt::Display for Machines { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (index, host) in self.list.iter().enumerate() { + write!(f, "{:<3}{}", index, host).unwrap(); + if index != self.list.len()-1 { // Hacky + write!(f, "\n").unwrap(); + } + } + return Ok(()); + } } // The Machine struct holds no information about where its json configuration file resides, if it @@ -68,26 +104,27 @@ impl Machines { // } // } +#[cfg(test)] mod tests { use super::*; #[test] fn init_machines() { - let m = Machines::new(); + let _machine_initialization_test = Machines::new(); } #[test] fn init_machines_and_add() { let mut m = Machines::new(); - m.add("Demo_Machine", "FF:FF:FF:FF:FF:FF"); + m.add("Demo_Machine", "FF:FF:FF:FF:FF:FF", None); assert_eq!(1, m.list.len()); - m.add("Demo_Machine2", "FF:FF:FF:FF:FF:FF"); + m.add("Demo_Machine2", "FF:FF:FF:FF:FF:FF", None); assert_eq!(2, m.list.len()); } #[test] fn write_and_load_from_file() { let mut m = Machines::new(); - m.add("File_Demo_Machine", "FF:FF:FF:FF:FF:FF"); + m.add("File_Demo_Machine", "FF:FF:FF:FF:FF:FF", None); assert_eq!(1, m.list.len()); let path = PathBuf::from("./DEMO_MACHINES.json"); std::fs::File::create(&path).unwrap(); diff --git a/src/main.rs b/src/main.rs index ab0cac1..d25577e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,28 @@ -#![allow(dead_code)] +// #![allow(dead_code)] +// #![allow(unused_imports)] + +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::PathBuf; +use std::error::Error; +use std::str::FromStr; // use std::{fs::{File, OpenOptions, metadata}, io::{Read, Write}, path::{Path, PathBuf}}; -use std::path::PathBuf; -use std::{error::Error, iter::Enumerate, string}; - // use serde::{Deserialize, Serialize}; //use serde_json::to; -mod cli_args; // Provides a custom function that specifies our command line options -mod host; // The actual Host struct -mod input; // Gives us a python-like input function, as well as a simple confirm function -mod machines; // Struct that holds a vec of Hosts, as well as operations on those -mod packet; // The actual magic packet struct, with wake methods e.t.c. -mod random_machine; // Exposes a function that generates a random Host, with random mac, ip and name // The actual host struct. +mod cli_args; // Provides a custom function that specifies our command line options +mod host; // The actual Host struct +mod input; // Gives us a python-like input function, as well as a simple confirm function +mod machines; // Struct that holds a vec of Hosts, as well as operations on those +mod packet; // The actual magic packet struct, with wake methods e.t.c. +// mod random_machine; // Exposes a function that generates a random Host, with random mac, ip and name // The actual host struct. +mod sanitizers; // Functions that sanitizes MAC and IP addresses // use crate::packet::*; use crate::machines::*; -use crate::packet::MagicPacket; -use random_machine::random_host; +use host::Host; +use input::*; // waker -a, --all // Wake all configured machines // waker -n, --name name1, name2 // Specified which configured name to wake @@ -26,31 +31,302 @@ use random_machine::random_host; // waker -e, --edit // Enters interactive editing mode // waker --backup-config [file] // Prints to stdout unless file is specified -enum RunMode { - Wake { mode: WakeMode }, - Edit { mode: EditMode }, +// Returned by cli argument parser (get_runmode()) +// This should later be matched in the main program to execute the corresponding functionality +/// Root enum for dictating program behaviour +pub enum RunMode { + Wake(WakeMode), + Edit, + Add, List, - ConfigBak, + Backup(BackupMode), } -/// Specifies how and which machines should be woken -enum WakeMode { - WakeAll, // Wake every configured machine - WakeSome { indexes: Vec }, // Wake machines with these indexes/ids - Direct { mac_strings: Vec }, // Wake these mac adresses, non-blocking +/// Specifies how and which machines should be wol'ed +pub enum WakeMode { + WakeAll, // Wake every configured machine + WakeSome, // Interactively pick hosts to wake + DirectMacs(Vec), // Wake these mac adresses, non-blocking } -/// Specifies how to perform edits -enum EditMode { - Pick, // Prompt the user for which machine to edit - Direct { name: String }, // Edit machine with specified name - DirectID { id: i32 }, // Edit machine with specified id +// /// Specifies how to perform edits +// pub enum EditMode { +// Pick, // Prompt the user for which machine to edit +// Direct(String), // Edit machine with specified name +// DirectID(i32), // Edit machine with specified id +// } + +// Describes how to edit a host +enum HostEditMode { + EditName, + EditIps, + EditMacs, } /// Specifies how the program should backup its config file -enum BackupMode { - ToFile { path: PathBuf }, // Write to file - ToStdout, // Write to stdout +pub enum BackupMode { + ToFile(String), // Write to file + ToStdout, // Write to stdout +} + +// fn prompt_file_creation(config_path: &PathBuf) -> Result<(), Box> { +fn prompt_file_creation(config_path: &PathBuf) -> Option { + let msg = format!( + "File \"{}\" does not seem to exist...\nCreate it?", + config_path.to_str().unwrap() + ); + if input::confirm(&msg) { + let newfile = std::fs::File::create(&config_path).expect(&format!( + "Could not create file: {}", + config_path.to_str().unwrap() + )); + + // Deserialize an empty Machines struct into the new config file. + // The parser is not equipped to handle empty or malformatted files... + // This could potentially be moved into a static method in Machines, like + // Machines::init_config_file(), which does this for us. + // + // For now, this will do. + let skeleton_machines = Machines::new(); + skeleton_machines.dump(&config_path).unwrap(); + return Some(newfile); + } else { + return None; + } +} + +// I think this one is pretty ok. It gets no points for readability, however. +// This could arguably be done with some kind of dictionary as +// well. A custom struct with a builder pattern would maybe work... +/// Presents a prompt. The user picks between the strings in the vector. The function returns an +/// Option containing the vector index of the picked element. +/// Returns None if input is empty, re-prompts if input invalid or index is out of range. +fn select_option(text: &str, options: &Vec) -> Option { + for (index, option) in options.iter().enumerate() { + println!("{:<5}{}", format!("{}.", index), option) + } + loop { + let input_str = input(&text); + if input_str.is_empty() { + return None; + } + match input_str.parse::() { + Ok(index) => { + if index >= 0 && (index as usize) < options.len() { + return Some(index); + } + return None; + } + Err(_what) => { + println!("Invalid input"); + } + } + } +} + +// INCOMPLETE +// Return Result<(), dyn Error> ? +/// Takes a host reference and a HostEditMode. +/// Behaves according to the HostEditMode provided. +fn edit_host(host: &mut Host, editmode: HostEditMode) { + println!("Works"); + match editmode { + HostEditMode::EditName => { + let new_name = input("New name: "); + if new_name.len() > 0 { + host.name = new_name; + } else { + println!("Name unchanged..."); + } + } + HostEditMode::EditIps => { + let select = select_option( + "What do you want to do?: ", + &vec![ + "Add an IP-address".to_string(), + "Edit an IP-address".to_string(), + "Remove an IP-address".to_string(), + ], + ); + match select { + Some(index) => { + match index { + 0 => { // Add + let newip = input("New IP: "); + match sanitizers::sanitize(&newip, sanitizers::AddrType::IPv4) { + Some(ip) => { + host.ips.push(ip); + } + None => { + println!("Could not parse IP"); + } + } + } + 1 => { // Edit + let select = select_option("Which ip?: ", &host.ips); + match select { + Some(index) => { + let newip = input("New IP: "); + match sanitizers::sanitize(&newip, sanitizers::AddrType::IPv4) { + Some(ip) => { + host.ips[index as usize] = newip; + } + None => { + println!("Could not parse IP"); + } + } + } + None => {} + } + } + 2 => { // Remove + let select = select_option("Which ip?: ", &host.ips); + match select { + Some(index) => { + host.ips.remove(index as usize); + } + None => {} + } + } + _ => {} + } + } + None => { + println!("None selected."); + } + } + } + HostEditMode::EditMacs => { + let select = select_option( + "What do you want to do?: ", + &vec![ + "Add a MAC-address".to_string(), + "Edit a MAC-address".to_string(), + "Remove a MAC-address".to_string(), + ], + ); + match select { + Some(index) => { + match index { + 0 => { // Add + let newmac = input("New MAC: "); + match sanitizers::sanitize(&newmac, sanitizers::AddrType::MAC) { + Some(mac_addr) => { + host.macs.push(mac_addr); + } + None => { + println!("Could not parse mac_addr"); + } + } + } + 1 => { // Edit + let select = select_option("Which MAC?: ", &host.macs); + match select { + Some(index) => { + let newmac = input("New MAC: "); + match sanitizers::sanitize(&newmac, sanitizers::AddrType::MAC) { + Some(mac_addr) => { + host.macs[index as usize] = newmac; + } + None => { + println!("Could not parse MAC"); + } + } + } + None => {} + } + } + 2 => { // Remove + let select = select_option("Which MAC?: ", &host.macs); + match select { + Some(index) => { + host.macs.remove(index as usize); + } + None => {} + } + } + _ => {} + } + } + None => { + println!("None selected."); + } + } + } + } +} + +// This code seems to be complete +/// Drops the user into a prompt for editing a host +fn edit_machines(machines: &mut Machines) { + loop { + println!("{}", machines); + let index_vec = which_indexes("Which host do you wish to edit? (Integer): ", machines); + match index_vec.len() { + 0 => break, + 1 => { + println!("Selected: {}", machines.list[index_vec[0] as usize].name); + let index = index_vec[0] as usize; + let host = &mut machines.list[index]; + println!("1. Name\n2. IP addresses\n3. Mac addresses\n4. Delete"); + let choice = parse_integers(&input("What would you like to edit? (Integer): ")); + match choice.len() { + 0 => break, + 1 => match choice[0] { + 1 => edit_host(host, HostEditMode::EditName), + 2 => edit_host(host, HostEditMode::EditIps), + 3 => edit_host(host, HostEditMode::EditMacs), + 4 => { + if confirm(&format!("Really delete host \"{}\"", machines.list[index].name)) { + machines.list.remove(index); + } + } + _ => break, + }, + _ => break, + } + } + _ => break, + } + println!("{:?}", index_vec); + } +} + +// Needs reworking. It works as intended but can be written significantly more elegant and +// efficient. +// TODO: In place searching and parsing +/// Parses any integer found in string into a vector of i32. +/// Integers can be arbitrarily separated +fn parse_integers(int_str: &String) -> Vec { + let mut return_vector = Vec::::new(); + let mut intstr = String::new(); + + for c in int_str.chars() { + let mut parse = false; + if c.is_digit(10) { + intstr.push(c); + } else { + parse = true; + } + + if !intstr.is_empty() && parse == true { + return_vector.push(intstr.parse().unwrap()); + intstr.clear(); + } + } + + if !intstr.is_empty() { + return_vector.push(intstr.parse().unwrap()); + intstr.clear(); + } + + return return_vector; +} + +fn which_indexes>(message: S, _machines: &Machines) -> Vec { + let indexes = input(message.as_ref()); + let integers = parse_integers(&indexes); + return integers; } fn main() -> Result<(), Box> { @@ -66,25 +342,8 @@ fn main() -> Result<(), Box> { // If file does not exist -> Ask to create it -> dump skeleton json into it if !config_path.is_file() { - let msg = format!( - "File \"{}\" does not seem to exist...\nCreate it?", - config_path.to_str().unwrap() - ); - if input::confirm(&msg) { - let newfile = std::fs::File::create(&config_path).expect(&format!( - "Could not create file: {}", - config_path.to_str().unwrap() - )); - - // Deserialize an empty Machines struct into the new config file. - // The parser is not equipped to handle empty or malformatted files... - // This could potentially be moved into a static method in Machines, like - // Machines::init_config_file(), which does this for us. - // - // For now, this will do. - let skeleton_machines = Machines::new(); - skeleton_machines.dump(&config_path); - } else { + let file = prompt_file_creation(&config_path); + if file.is_none() { println!("Exiting..."); return Ok(()); } @@ -92,19 +351,144 @@ fn main() -> Result<(), Box> { // TODO: More sophisticated error checking and logging let mut machines = Machines::from_json_file(&config_path)?; - // let rhost = random_host(); - // machines.add("test", "FF:FF:FF:FF:FF:FF"); - // machines.add(&rhost.name, &rhost.macs[0]); // Hack, needs to have a method for this.. + + // Figure out how the program should behave + let run_mode = cli_args::get_runmode(); - for (index, machine) in machines.list.iter().enumerate() { - let macs_str = format!("{:?}", machine.macs); - println!("{:<3}{:25}{}", index, macs_str, machine.name); // TODO: CHANGE THIS FORMAT TO INDEX, NAME, MACS - for mac in &machine.macs { - let mp = MagicPacket::from_str(&mac).expect("Could not parse mac address..."); - mp.send(); + match run_mode { + RunMode::List => { + println!("{}", machines); + } + RunMode::Wake(wake_mode) => { + match wake_mode { + WakeMode::WakeAll => { + machines.wakeall(); + for host in &machines.list { + println!("Woke {}", host.name) + } + } + WakeMode::WakeSome => { + println!("{}", machines); + let indexes = which_indexes( + "Select which hosts to wake up (Comma separated integers): ", + &machines, + ); + for index in indexes { + // TODO: Bounds checking + let host = &machines.list[index as usize]; + host.wake(); + println!("Woke {}", host.name) + } + } + _ => println!("undefined"), + } + } + RunMode::Edit => { + edit_machines(&mut machines); + } + RunMode::Add => { + println!("Add new machine:"); + let mut add_machine: bool = true; + let mut name: String = String::from(""); + let mut mac_addr: String = String::from(""); + let mut ip_addr: Option = None; + while add_machine { + name = input("What would you like to call your host?:\n"); + if name.is_empty() { + add_machine = false; + break; + } + if confirm(&format!("Name: {}, is this correct?", &name)) { + break; + } + } + while add_machine { + mac_addr = input("What MAC address is assigned to your host?:\n"); + if mac_addr.is_empty() { + add_machine = false; + break; + } + if confirm(&format!("MAC: {}, is this correct?", &mac_addr)) { + break; + } + } + while add_machine { + let ip_str = input("What IP address is assigned to your host?: (Blank for none)\n"); + if ip_str.is_empty() { + ip_addr = None; + break; + } + if confirm(&format!("IP: {}, is this correct?", &ip_str)) { + ip_addr = Some(ip_str); + break; + } + } + if add_machine { + machines.add(&name, &mac_addr, ip_addr); + } + } + // Might need some polish in regards to guards and error handling. + // Perhaps there is a cleaner way to do the writing... + // This seems to work fine for now + RunMode::Backup(backup_mode) => { + // Read entire file unbuffered into memory. + let content = fs::read_to_string(&config_path).unwrap(); + match backup_mode { + // Another valid, and maybe more concise way of doing this is to just do a plain + // copy of the config file into the *valid* file_string destination. + // This does its job "good enough"(TM) for now... + BackupMode::ToFile(file_string) => { + let backup_file = PathBuf::from_str(&file_string).unwrap(); + // If the target file does not already exist, and is not a directory + // TODO: Further guards + if !backup_file.exists() && !backup_file.is_dir() { + println!("Executing file backup"); + let mut backup_file_handle = OpenOptions::new().create(true).write(true).open(backup_file).unwrap(); + backup_file_handle.write_all(content.as_bytes()).unwrap(); + } + else if backup_file.exists() && backup_file.is_file() { + if confirm(&format!("The file \"{}\" already exists...\nOverwrite?", &file_string)) { + let mut backup_file_handle = OpenOptions::new().write(true).open(backup_file).unwrap(); + backup_file_handle.write_all(content.as_bytes()).unwrap(); + } + } + else { + // Well, what else should i do? If a user enters a directory as backup + // file, id consider that a dead end. + // + // Some other obscure errors that i havent considered might end up in this branch as well. + println!("Invalid path... Exiting"); + } + }, + BackupMode::ToStdout => { + for line in content.lines() { + println!("{}", line); + } + }, + } } } machines.dump(&config_path)?; return Ok(()); } + +#[cfg(test)] +mod main_test { + use super::*; + #[test] + fn parse_ints_test() { + let numvec = vec![10, 20, 30, 2, 4, 1923]; + let numstr = format!("{:?}", numvec); + let numstr2 = String::from("10kfsa20fav?30!::]2sd4::;sdalkd c 1923"); // Seriously borked input + let parsed_vec = parse_integers(&numstr); + let parsed_vec2 = parse_integers(&numstr2); + + assert_eq!(numvec, parsed_vec); + assert_eq!(numvec, parsed_vec2); + + println!("{:?}", numvec); + println!("{:?}", parsed_vec); + println!("{:?}", parsed_vec2); + } +} diff --git a/src/packet.rs b/src/packet.rs index 3c4c2dd..1a7c87c 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -63,6 +63,13 @@ impl MagicPacket { return Ok(MagicPacket::new(&MagicPacket::parse(mac_str).unwrap())); } + // Stolen + fn mac_to_byte(data: &str, sep: char) -> Vec { + data.split(sep) + .flat_map(|x| hex::decode(x).expect("Invalid mac!")) + .collect() + } + // Parses string by position if string is 12+5 characters long (delimited by : for example) pub fn parse>(mac_str: S) -> Result, Box> { use hex::FromHex; diff --git a/src/random_machine.rs b/src/random_machine.rs index f07a0fc..bd46c44 100644 --- a/src/random_machine.rs +++ b/src/random_machine.rs @@ -1,7 +1,7 @@ -use eff_wordlist::large::random_word; use crate::host::Host; -use rand::thread_rng; +use eff_wordlist::large::random_word; use rand::prelude::*; +use rand::thread_rng; // This file exists purely for debugging/testing purposes @@ -10,13 +10,8 @@ fn random_mac() -> String { let bytes: [u8; 6] = rng.gen(); // rand can handle array initialization let mac_str = format!( "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", - bytes[0], - bytes[1], - bytes[2], - bytes[3], - bytes[4], - bytes[5], - ); + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], + ); return mac_str; } @@ -24,13 +19,7 @@ fn random_ip() -> String { let mut rng = thread_rng(); let bytes: [u8; 4] = rng.gen(); // rand can handle array initialization - let mut ip_str = format!( - "{}.{}.{}.{}", - bytes[0], - bytes[1], - bytes[2], - bytes[3], - ); + let ip_str = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3],); return ip_str; } @@ -45,6 +34,7 @@ pub fn random_host() -> Host { return Host::new(random_name(), random_mac(), random_ip()); } +#[cfg(test)] mod tests { use super::*; #[test] diff --git a/src/sanitizers.rs b/src/sanitizers.rs new file mode 100644 index 0000000..b22555b --- /dev/null +++ b/src/sanitizers.rs @@ -0,0 +1,73 @@ +/// For use as parameter in the sanitize function +pub enum AddrType { + MAC, + IPv4, +} + +/// Takes an AddrType enum and returns an Option containing the sanitized string. +/// Returns None if failed to sanitize. +/// This is a very permissive sanitizer, and it will parse even the most malformatted strings +/// It is guaranteed to return a valid MAC/IP +pub fn sanitize(address: &str, addr_type: AddrType) -> Option { + match addr_type { + AddrType::MAC => { + let mut mac_str = String::new(); + for c in address.to_string().chars() { + if c.is_digit(16) { + mac_str.push(c); + if mac_str.len() == 12 { break; } + } + } + mac_str.insert(10, ':'); + mac_str.insert(8, ':'); + mac_str.insert(6, ':'); + mac_str.insert(4, ':'); + mac_str.insert(2, ':'); + return Some(mac_str); + } + AddrType::IPv4 => { + let mut bytes: Vec = Vec::new(); // Explicit type to avoid any typing errors + let ip_str = address.to_string(); + let byte_list = ip_str.split('.'); + for byte_base10_str in byte_list { + match byte_base10_str.parse::() { + Ok(byte) => bytes.push(byte), + Err(_what) => continue, + } + // bytes.push(byte_base10_str.parse::().unwrap()); + } + return Some(format!("{}.{}.{}.{}", bytes[0], bytes[2], bytes[2], bytes[3])); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn sanitize_mac() { + let macstr = String::from("FFFFFFFFFFFF"); + let formatted = String::from("FF:FF:FF:FF:FF:FF"); + assert_eq!(formatted, sanitize(&macstr, AddrType::MAC).unwrap()); + } + + #[test] + // No comparison check, just check so the length is correct + fn sanitize_mac_garbage() { + let macstr = String::from("sdakjaojoiwjvoievoijevioqjoijeriojkljlknxxx218913981389981jixjxxjk1kj1k"); + assert_eq!(17, sanitize(&macstr, AddrType::MAC).unwrap().len()); + } + #[test] + fn sanitize_ip() { + let ipstr = String::from("255.255.255.255"); + let formatted = String::from("255.255.255.255"); + assert_eq!(formatted, sanitize(&ipstr, AddrType::IPv4).unwrap()); + } + + #[test] + fn sanitize_ip_garbage() { + let ipstr = String::from("asdafasd.255.255.asdakfjjkjkfjk.255.255.asdafa"); + let formatted = String::from("255.255.255.255"); + assert_eq!(formatted, sanitize(&ipstr, AddrType::IPv4).unwrap()); + } +} diff --git a/waker.json b/waker.json index 615cf24..4320a45 100644 --- a/waker.json +++ b/waker.json @@ -1,3 +1,32 @@ { - "list": [] + "list": [ + { + "name": "Name", + "macs": [ + "E9:A3:29:73:FE:D2" + ], + "ips": [] + }, + { + "name": "i", + "macs": [ + "7D:E1:AC:94:29:79" + ], + "ips": [] + }, + { + "name": "PartyMaskinen", + "macs": [ + "96:F8:FF:30:4C:EE" + ], + "ips": [] + }, + { + "name": "Kalle", + "macs": [ + "FF:FF:FF:FF:FF:FF" + ], + "ips": [] + } + ] } \ No newline at end of file