Cli implemented, somewhat complete functionality
This commit is contained in:
		
							parent
							
								
									4bd8c3984d
								
							
						
					
					
						commit
						44e63e679e
					
				
					 10 changed files with 696 additions and 100 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1 +1,2 @@ | |||
| /target | ||||
| waker.json | ||||
|  |  | |||
							
								
								
									
										11
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								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. | ||||
|  |  | |||
|  | @ -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(); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										20
									
								
								src/host.rs
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								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<String>, | ||||
|     // pub ips: Vec<String>,
 | ||||
|     pub ips: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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) { | ||||
|     // 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<String>) { | ||||
|         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<Machines, Box<dyn Error>> { | ||||
|         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<dyn Error>> { | ||||
|         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<bool, Box<dyn Error>> { | ||||
|         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(); | ||||
|  |  | |||
							
								
								
									
										474
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										474
									
								
								src/main.rs
									
										
									
									
									
								
							|  | @ -1,9 +1,13 @@ | |||
| #![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;
 | ||||
| 
 | ||||
|  | @ -12,12 +16,13 @@ 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 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,46 +31,46 @@ 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 { | ||||
| /// Specifies how and which machines should be wol'ed
 | ||||
| pub enum WakeMode { | ||||
|     WakeAll,                 // Wake every configured machine
 | ||||
|     WakeSome    { indexes: Vec<i32> },        // Wake machines with these indexes/ids
 | ||||
|     Direct      { mac_strings: Vec<String> }, // Wake these mac adresses, non-blocking
 | ||||
|     WakeSome,                // Interactively pick hosts to wake
 | ||||
|     DirectMacs(Vec<String>), // 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
 | ||||
| pub enum BackupMode { | ||||
|     ToFile(String), // Write to file
 | ||||
|     ToStdout,       // Write to stdout
 | ||||
| } | ||||
| 
 | ||||
| fn main() -> Result<(), Box<dyn Error>> { | ||||
|     let config_path = match cfg!(debug_assertions) { | ||||
|         // If this is a debug build, the the path becomes ./waker.json, relative to project root
 | ||||
|         true => PathBuf::new().join("waker.json"), | ||||
| 
 | ||||
|         // If this is a release build, this is essentially ~/.config/waker.json stored in a pathbuf object
 | ||||
|         false => dirs::config_dir() | ||||
|             .expect("Could not find config directory...") | ||||
|             .join("waker.json"), | ||||
|     }; | ||||
| 
 | ||||
|     // If file does not exist -> Ask to create it -> dump skeleton json into it
 | ||||
|     if !config_path.is_file() { | ||||
| // fn prompt_file_creation(config_path: &PathBuf) -> Result<(), Box<dyn Error>> {
 | ||||
| fn prompt_file_creation(config_path: &PathBuf) -> Option<File> { | ||||
|     let msg = format!( | ||||
|         "File \"{}\" does not seem to exist...\nCreate it?", | ||||
|         config_path.to_str().unwrap() | ||||
|  | @ -83,8 +88,262 @@ fn main() -> Result<(), Box<dyn Error>> { | |||
|         //
 | ||||
|         // For now, this will do.
 | ||||
|         let skeleton_machines = Machines::new(); | ||||
|             skeleton_machines.dump(&config_path); | ||||
|         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<i32> 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<String>) -> Option<i32> { | ||||
|     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::<i32>() { | ||||
|             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<i32> { | ||||
|     let mut return_vector = Vec::<i32>::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<S: AsRef<str>>(message: S, _machines: &Machines) -> Vec<i32> { | ||||
|     let indexes = input(message.as_ref()); | ||||
|     let integers = parse_integers(&indexes); | ||||
|     return integers; | ||||
| } | ||||
| 
 | ||||
| fn main() -> Result<(), Box<dyn Error>> { | ||||
|     let config_path = match cfg!(debug_assertions) { | ||||
|         // If this is a debug build, the the path becomes ./waker.json, relative to project root
 | ||||
|         true => PathBuf::new().join("waker.json"), | ||||
| 
 | ||||
|         // If this is a release build, this is essentially ~/.config/waker.json stored in a pathbuf object
 | ||||
|         false => dirs::config_dir() | ||||
|             .expect("Could not find config directory...") | ||||
|             .join("waker.json"), | ||||
|     }; | ||||
| 
 | ||||
|     // If file does not exist -> Ask to create it -> dump skeleton json into it
 | ||||
|     if !config_path.is_file() { | ||||
|         let file = prompt_file_creation(&config_path); | ||||
|         if file.is_none() { | ||||
|             println!("Exiting..."); | ||||
|             return Ok(()); | ||||
|         } | ||||
|  | @ -92,19 +351,144 @@ fn main() -> Result<(), Box<dyn Error>> { | |||
| 
 | ||||
|     // 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..
 | ||||
|     
 | ||||
|     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(); | ||||
|     // Figure out how the program should behave
 | ||||
|     let run_mode = cli_args::get_runmode(); | ||||
| 
 | ||||
|     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<String> = 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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<u8> { | ||||
|         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<S: AsRef<str>>(mac_str: S) -> Result<Box<[u8; 6]>, Box<dyn Error>> { | ||||
|         use hex::FromHex; | ||||
|  |  | |||
|  | @ -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,12 +10,7 @@ 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] | ||||
|  |  | |||
							
								
								
									
										73
									
								
								src/sanitizers.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/sanitizers.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<String> 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<String> { | ||||
|     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<u8> = 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::<u8>() { | ||||
|                     Ok(byte) => bytes.push(byte), | ||||
|                     Err(_what) => continue, | ||||
|                 } | ||||
|                 // bytes.push(byte_base10_str.parse::<u8>().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()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										31
									
								
								waker.json
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								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": [] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Imbus
						Imbus