commit 4bd8c3984dab5ec7b389247d3eec765f1a4f1aaf Author: Imbus Date: Tue Jun 29 15:51:57 2021 +0200 Working proof of concept diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4ce6d51 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,458 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "eff-wordlist" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226cc2588a6b769f444d06dedd29a366ea92469edea1cef7873fbd76e5b34c24" +dependencies = [ + "rand 0.6.5", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "waker" +version = "0.1.0" +dependencies = [ + "clap", + "dirs", + "eff-wordlist", + "hex", + "rand 0.8.4", + "serde", + "serde_json", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4b97ec5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "waker" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hex = "*" +serde = { version = "*", features = ["derive"] } +serde_json = "*" +dirs = "*" +clap = "*" +eff-wordlist = "*" +rand = "*" diff --git a/README.md b/README.md new file mode 100644 index 0000000..f628d1d --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +TODO: Review parsing methods + +Hosts should be pinged on listing + +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. + +102 bytes + +several macs per name + +edit, delete, add + +wakemode::all + +wake(bytes) method +async ping method(ip)? + +const MAGIC_BYTES_HEADER: [u8; 6] = [0xFF; 6]; +vec bytes = FFFFFFFFFFFF .reserve(enough) 102? + +match run_mode { + Some() => Do according to some + None => Figure out what to do(set run_mode) +} + +add 8 char string -> to bytes ask for name and add +add 11 char string -> if mac[2,5,8] is same char -> split by it and ask for name -> add + +store json in ~/.config/waker.conf +dump-json method +export/import? append? + +struct mechine { + vec of all macs + ip of machine? so we can ping it? + name +} + +struct mac_list { + Vec +} + + +ask what do: + 1. Wake + 2. Wake all + 3. Add + 4. Edit diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..45222aa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ + enum_discrim_align_threshold = 20 + struct_field_align_threshold = 20 diff --git a/src/cli_args.rs b/src/cli_args.rs new file mode 100644 index 0000000..c6f0f48 --- /dev/null +++ b/src/cli_args.rs @@ -0,0 +1,49 @@ +use clap::{App, Arg, ArgMatches}; + +// use crate::main::RunMode; + +// let a = RunMode; +// waker FF:FF:FF:FF:FF:FF // Direct mode, wakes the given mac address +// waker -a, --all // Wake all configured machines +// waker -n, --name name1, name2 // Specified which configured name to wake +// waker -m, --macs mac1, mac2 // Specifies which macs to send magic packet to in direct mode +// waker -l, --list // Lists all configured machines +// waker -e, --edit // Enters interactive editing mode +// waker --backup-config [file] // Prints to stdout unless file is specified + +pub fn get_cli_matches() -> ArgMatches<'static> { + /* Move this out to a function that returns a config struct with all the + * options */ + /* or just return the ArgMatches object for clarity */ + return App::new("Waker") + //.version("0.01") + .version(env!("CARGO_PKG_VERSION")) + .author("Imbus64") + .about("Utility for sending magic packets to configured machines.") + .arg( + Arg::with_name("all") + .short("a") + .long("all") + .help("Wake all configured hosts"), + ) + .arg( + Arg::with_name("list") + .short("l") + .long("list") + .conflicts_with("weight") + .help("Print all entries"), + ) + .arg( + Arg::with_name("raw") + .long("raw") + .conflicts_with_all(&["list", "plain"]) + .help("Print raw log file to stdout"), + ) + .arg( + Arg::with_name("plain") + .long("plain") + .conflicts_with_all(&["list", "raw"]) + .help("Print all entries without pretty table formatting"), + ) + .get_matches(); +} diff --git a/src/host.rs b/src/host.rs new file mode 100644 index 0000000..3356afc --- /dev/null +++ b/src/host.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Host { + pub name: String, + pub macs: Vec, + // pub ips: Vec, +} + +impl Host { + pub fn new>(name: S, mac: S, ipv4: S) -> Host { + Host { + name: name.into(), + macs: vec![mac.into()], + // ips: vec![ipv4], + } + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..17166c2 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,25 @@ +use std::io; +use std::io::prelude::*; + +/// Python like input method with prompt message +pub fn input(prompt: &str) -> String { + print!("{}", prompt); + let _ = io::stdout().flush(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .expect("Could not read line."); + + // Remove leading and trailing whitespaces and return + input.trim().to_string() +} + +/// Simple confirm dialogue +pub fn confirm(message: &str) -> bool { + let answer = input(format!("{} [y/N]: ", message).as_str()).to_uppercase(); + if answer == "YES" || answer == "Y" { + return true; + } + return false; +} diff --git a/src/machines.rs b/src/machines.rs new file mode 100644 index 0000000..3f9435b --- /dev/null +++ b/src/machines.rs @@ -0,0 +1,104 @@ +// use std::{fs::{File, OpenOptions, metadata}, io::{Read, Write}, path::{Path, PathBuf}}; +use std::error::Error; +use std::{ + fs::{File, OpenOptions}, + io::Write, + path::PathBuf, +}; + +use crate::host::Host; +use serde::{Deserialize, Serialize}; + +// Possibly rename to HostList +#[derive(Serialize, Deserialize)] +pub struct Machines { + pub list: Vec, +} + +impl Machines { + pub fn new() -> Machines { + Machines { + list: Vec::::new(), + } + } + + pub fn add(&mut self, name: &str, mac_addr: &str) { + self.list.push(Host { + name: name.to_string(), + macs: vec![mac_addr.to_string()], + }); + } + + 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) + } + + /// 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)?; + let mut file = OpenOptions::new() + .write(true) + .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) + } +} + +// The Machine struct holds no information about where its json configuration file resides, if it +// did we could deserialize it into it in the destructor... +// impl Drop for Machines { +// fn drop(&mut self) { +// println!("Destructor called..."); +// } +// } + +mod tests { + use super::*; + #[test] + fn init_machines() { + let m = Machines::new(); + } + + #[test] + fn init_machines_and_add() { + let mut m = Machines::new(); + m.add("Demo_Machine", "FF:FF:FF:FF:FF:FF"); + assert_eq!(1, m.list.len()); + m.add("Demo_Machine2", "FF:FF:FF:FF:FF:FF"); + 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"); + assert_eq!(1, m.list.len()); + let path = PathBuf::from("./DEMO_MACHINES.json"); + std::fs::File::create(&path).unwrap(); + m.dump(&path).unwrap(); + // println!("{}", path. to_str().unwrap()); + let m2 = Machines::from_json_file(&path).unwrap(); + + assert_eq!(1, m2.list.len()); + + // This cleanup will never happen if the previous assert fails... + // TODO: Branch eq check to a panic instead + std::fs::remove_file(&path).unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ab0cac1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,110 @@ +#![allow(dead_code)] + +// 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. + +// use crate::packet::*; +use crate::machines::*; +use crate::packet::MagicPacket; +use random_machine::random_host; + +// waker -a, --all // Wake all configured machines +// waker -n, --name name1, name2 // Specified which configured name to wake +// waker -m, --macs mac1, mac2 // Specifies which macs to send magic packet to in direct mode +// waker -l, --list // Lists all configured machines +// 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 }, + List, + ConfigBak, +} + +/// 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 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 the program should backup its config file +enum BackupMode { + ToFile { path: PathBuf }, // Write to file + ToStdout, // Write to stdout +} + +fn main() -> Result<(), Box> { + 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 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 { + println!("Exiting..."); + return Ok(()); + } + } + + // 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(); + } + } + + machines.dump(&config_path)?; + return Ok(()); +} diff --git a/src/packet.rs b/src/packet.rs new file mode 100644 index 0000000..3c4c2dd --- /dev/null +++ b/src/packet.rs @@ -0,0 +1,162 @@ +use std::{ + error::Error, + net::{Ipv4Addr, ToSocketAddrs, UdpSocket}, +}; + +// The format of a Wake-on-LAN (WOL) magic packet is defined +// as a byte array with 6 bytes of value 255 (0xFF) followed by +// 16 repetitions of the target machine’s 48-bit (6-byte) MAC address. +// This becomes a total of 102 bytes: + +const MAGIC_HEADER: [u8; 6] = [0xFF; 6]; + +/// Contains raw bytes for magic packet +pub struct MagicPacket { + pub bytes: [u8; 102], +} + +impl MagicPacket { + /// Create new MagicPacket from a raw 6-byte MAC address + pub fn new(mac_bytes: &[u8; 6]) -> MagicPacket { + let mut magic_bytes: [u8; 102]; + unsafe { + magic_bytes = std::mem::MaybeUninit::uninit().assume_init(); + let mut src: *const u8 = &MAGIC_HEADER[0]; + let mut dst: *mut u8 = &mut magic_bytes[0]; + dst.copy_from_nonoverlapping(src, 6); + + src = &mac_bytes[0]; + for _ in 0..16 { + dst = dst.offset(6); + dst.copy_from_nonoverlapping(src, 6); + } + } + return MagicPacket { bytes: magic_bytes }; + } + + // This is horrible code + /// Parse a MAC-string into a packet. + /// Takes both separated and unseparated + pub fn from_str(mac_str: &str) -> Result> { + use hex::FromHex; + let mut mac_string = mac_str.trim().to_string(); + + if mac_string.len() == 17 { + for i in 0..5 { + mac_string.remove(14 - (i * 3)); // Deal with it + } + } + + let mut mac_arr: [u8; 6]; + let hex_vec = Vec::from_hex(mac_string)?; + + unsafe { + mac_arr = std::mem::MaybeUninit::uninit().assume_init(); + let src: *const u8 = &hex_vec[0]; + let dst: *mut u8 = &mut mac_arr[0]; + dst.copy_from_nonoverlapping(src, 6); + } + return Ok(MagicPacket::new(&mac_arr)); + } + + pub fn from_str2(mac_str: &str) -> Result> { + return Ok(MagicPacket::new(&MagicPacket::parse(mac_str).unwrap())); + } + + // 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; + let mut mstr: &str = mac_str.as_ref(); + mstr = mstr.trim(); + let mut bytes: [u8; 6] = [0, 0, 0, 0, 0, 0]; + + for (index, byte) in bytes.iter_mut().enumerate() { + // 0,3,6,9... + let substr = &mstr[3 * index..3 * index + 2]; + *byte = <[u8; 1]>::from_hex(substr).unwrap()[0]; + } + + return Ok(Box::new(bytes)); + } + + // Loops the string and parses any valid hex + pub fn parse_harder>(mac_str: S) -> Result, Box> { + let mstr: String = mac_str.into(); + let mut hexdigits: String = mstr + .chars() + .filter(|c| char::is_ascii_hexdigit(c)) + .collect(); + let mut bytes: [u8; 6] = [0; 6]; + + if hexdigits.len() >= 12 { + hexdigits.truncate(12); // May not be needed, since bytes is only 6 bytes, and from_hex might be smart enough to realize... + //let bytes: [u8; 6] = hex::FromHex::from_hex(hexdigits)?; + bytes = hex::FromHex::from_hex(hexdigits)?; + } + return Ok(Box::new(bytes)); + } + + /// Send packet to/from specific address/interface + pub fn send_to(&self, to_addr: A, from_addr: A) -> std::io::Result<()> { + let socket = UdpSocket::bind(from_addr)?; + socket.set_broadcast(true)?; + socket.send_to(&self.bytes, to_addr)?; + Ok(()) + } + + /// Send package from whatever interface the os picks + pub fn send(&self) -> std::io::Result<()> { + self.send_to( + (Ipv4Addr::new(0xFF, 0xFF, 0xFF, 0xFF), 9), + (Ipv4Addr::new(0x0, 0x0, 0x0, 0x0), 0), + ) + } + + /// Return raw bytes, ready to be broadcasted + pub fn get_bytes(&self) -> &[u8; 102] { + &self.bytes + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn init_packet() { + let packet1 = MagicPacket::new(&[0xFF; 6]); + let packet2 = MagicPacket::new(&[0xAA; 6]); + + let vec: [u8; 102] = [0xFF; 102]; + + assert_eq!(packet1.bytes, vec); + assert_ne!(packet2.bytes, vec); + } + + #[test] + fn magic_packet_from_new() { + let packet3 = MagicPacket::from_str2("10:10:10:10:10:10").unwrap(); + //let vec: [u8; 102] = [0xFF; 102]; + let slice = &packet3.bytes[packet3.bytes.len() - 6..]; + for a in slice { + println!("{}", a); + } + + let bytes: [u8; 6] = [0x10; 6]; + assert_eq!(slice, bytes); + } + + #[test] + fn test_parse() { + let mp = MagicPacket::parse("ff:ff:ff:ff:ff:ff").unwrap(); + assert_eq!([0xFF; 6], *mp); + let mp2 = MagicPacket::parse("10:10:10:10:10:10").unwrap(); + assert_eq!([0x10; 6], *mp2); + } + + #[test] + fn parse_harder_test() { + let mac = MagicPacket::parse_harder("10:10:10:10:10:10").unwrap(); + assert_eq!([0x10; 6], *mac); + } +} diff --git a/src/random_machine.rs b/src/random_machine.rs new file mode 100644 index 0000000..f07a0fc --- /dev/null +++ b/src/random_machine.rs @@ -0,0 +1,94 @@ +use eff_wordlist::large::random_word; +use crate::host::Host; +use rand::thread_rng; +use rand::prelude::*; + +// This file exists purely for debugging/testing purposes + +fn random_mac() -> String { + let mut rng = thread_rng(); + 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], + ); + return mac_str; +} + +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], + ); + return ip_str; +} + +fn random_name() -> String { + let mut name = String::new(); + name.push_str(random_word()); + name.push_str(random_word()); + return name; +} + +pub fn random_host() -> Host { + return Host::new(random_name(), random_mac(), random_ip()); +} + +mod tests { + use super::*; + #[test] + fn test_random_name() { + let mut testvec: Vec = Vec::new(); + for _ in 0..10 { + // println!("{}", random_name()); + let test_str = random_name(); + println!("{}", test_str); + for other in &testvec { + assert!(!other.eq(&test_str)); // Make sure this particular string is not already present in testvec + } + testvec.push(test_str); + } + } + + #[test] + fn test_random_ip() { + let mut testvec: Vec = Vec::new(); + for _ in 0..10 { + // println!("{}", random_name()); + let test_str = random_ip(); + println!("{}", test_str); + assert!(test_str.len() >= 7 && test_str.len() <= 15); + for other in &testvec { + assert!(!other.eq(&test_str)); // Make sure this particular string is not already present in testvec + } + testvec.push(test_str); + } + } + + #[test] + fn test_random_mac() { + let mut testvec: Vec = Vec::new(); + for _ in 0..10 { + // println!("{}", random_name()); + let test_str = random_mac(); + println!("{}", test_str); + // assert!(test_str.len() >= 7 && test_str.len() <= 15); + assert!(test_str.len() == 17); + for other in &testvec { + assert!(!other.eq(&test_str)); // Make sure this particular string is not already present in testvec + } + testvec.push(test_str); + } + } +} diff --git a/waker.json b/waker.json new file mode 100644 index 0000000..615cf24 --- /dev/null +++ b/waker.json @@ -0,0 +1,3 @@ +{ + "list": [] +} \ No newline at end of file