From 172f8163139f8112b76d462198a1213a5cb49dde Mon Sep 17 00:00:00 2001 From: Gus Power Date: Sat, 7 Jun 2025 14:55:21 +0100 Subject: first test of Junie - add network module for detecting available network adapters --- Cargo.lock | 130 +++++++++++++++++++++++++++++++++++++++++---------------- Cargo.toml | 15 ++++++- src/error.rs | 12 +++++- src/http.rs | 46 ++++++++++++++++---- src/lib.rs | 11 +++++ src/main.rs | 3 +- src/network.rs | 93 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 262 insertions(+), 48 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/network.rs diff --git a/Cargo.lock b/Cargo.lock index ebb67ab..65c765d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,12 +82,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -165,9 +165,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.23" +version = "1.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ "shlex", ] @@ -674,11 +674,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -707,9 +706,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ "bytes", "futures-channel", @@ -737,7 +736,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.1", + "windows-core 0.61.2", ] [[package]] @@ -798,9 +797,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -814,9 +813,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -904,9 +903,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -917,9 +916,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", @@ -983,13 +982,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1002,6 +1001,7 @@ dependencies = [ "homedir", "http", "log", + "network-interface", "reqwest", "serde", "serde_json", @@ -1028,6 +1028,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "network-interface" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3329f515506e4a2de3aa6e07027a6758e22e0f0e8eaf64fa47261cec2282602" +dependencies = [ + "cc", + "libc", + "thiserror", + "winapi", +] + [[package]] name = "nix" version = "0.29.0" @@ -1080,6 +1092,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.72" @@ -1352,9 +1370,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -1604,6 +1622,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.41" @@ -1647,9 +1685,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -1909,6 +1947,28 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +[[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" + [[package]] name = "windows" version = "0.57.0" @@ -1933,15 +1993,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", "windows-link", - "windows-result 0.3.3", - "windows-strings 0.4.1", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -2000,7 +2060,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.3", + "windows-result 0.3.4", "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -2016,9 +2076,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -2034,9 +2094,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index de42a63..2262aa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,18 @@ description = "A command line interface (CLI) tool for syncing public WAN ip add repository = "https://github.com/guspower/multiwan-dyndns" license = "MIT OR Apache-2.0" +# Both a library and a binary +[[bin]] +name = "multiwan-dyndns" +path = "src/main.rs" + +[lib] +name = "multiwan_dyndns" +path = "src/lib.rs" + [dependencies] reqwest = "0.12.15" -tokio = { version = "1.45.0", features = ["rt", "rt-multi-thread", "macros"] } +tokio = { version = "1.45.1", features = ["rt", "rt-multi-thread", "macros"] } http = { version = "1.3.1" } serde_json = "1.0.140" serde = { version = "1.0.219", features = ["derive"] } @@ -19,6 +28,10 @@ env_logger = "0.11.8" clap = { version = "4.5.38", features = ["derive", "string"] } log = "0.4.27" homedir = "0.3.4" +network-interface = "2.0.1" [dev-dependencies] wiremock = "0.6.3" + +[profile.dev] +debug = false diff --git a/src/error.rs b/src/error.rs index 1ce9482..1b71511 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ use homedir::GetHomeError; +use network_interface::Error as NetworkInterfaceError; use reqwest::{Error as ReqwestError, Url}; use serde_json::Error as JsonError; use std::fmt; @@ -16,6 +17,7 @@ pub enum AppError { InvalidResponse { url: Url, reason: String }, InvalidHttpHeader(String), UnableToGetHomeDirectory(GetHomeError), + NetworkInterfaceError(NetworkInterfaceError), } impl fmt::Display for AppError { @@ -34,11 +36,16 @@ impl fmt::Display for AppError { Self::RequestFailed { url, .. } => write!(f, "Request to {} failed", url), Self::InvalidResponse { url, reason } => { write!(f, "Invalid response from {}: {}", url, reason) - }, - Self::InvalidHttpHeader(message) => write!(f, "Invalid HTTP header configuration: {}", message), + } + Self::InvalidHttpHeader(message) => { + write!(f, "Invalid HTTP header configuration: {}", message) + } Self::UnableToGetHomeDirectory(err) => { write!(f, "Failed to get home directory: {}", err) } + Self::NetworkInterfaceError(err) => { + write!(f, "Network interface error: {}", err) + } } } } @@ -49,6 +56,7 @@ impl std::error::Error for AppError { Self::IoError(err) => Some(err), Self::ConfigParseError { source, .. } => Some(source), Self::RequestFailed { source, .. } => Some(source), + Self::NetworkInterfaceError(err) => Some(err), _ => None, } } diff --git a/src/http.rs b/src/http.rs index db5e3a6..4ea9b96 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,9 +1,9 @@ -use serde_with::DisplayFromStr; +use crate::error::{AppError, AppResult}; use http::{HeaderMap, HeaderName, HeaderValue, Method}; use reqwest::{Client, Request, Url}; use serde::{Deserialize, Serialize}; +use serde_with::DisplayFromStr; use serde_with::serde_as; -use crate::error::{AppError, AppResult}; #[serde_as] #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -18,9 +18,12 @@ pub struct RequestConfig { } impl RequestConfig { - pub fn new(method: Method, url: Url, headers: Vec<(String, String)>) -> Self { - Self { method, url, headers } + Self { + method, + url, + headers, + } } pub fn get(url: Url, headers: Vec<(String, String)>) -> Self { @@ -32,6 +35,8 @@ impl RequestConfig { if !self.headers.is_empty() { builder = builder.headers(self.header_map()?); } + builder = builder.body("OHAI"); + Ok(builder.build()?) } @@ -43,25 +48,30 @@ impl RequestConfig { let mut header_map = HeaderMap::new(); for (name, value) in &self.headers { let name: HeaderName = name.try_into().map_err(|e| { - AppError::InvalidHttpHeader(format!("[{}] is not a valid HTTP header name ({})", name, e)) + AppError::InvalidHttpHeader(format!( + "[{}] is not a valid HTTP header name ({})", + name, e + )) })?; let value: HeaderValue = value.parse().map_err(|e| { - AppError::InvalidHttpHeader(format!("[{}] is not a valid HTTP header value ({})", value, e)) + AppError::InvalidHttpHeader(format!( + "[{}] is not a valid HTTP header value ({})", + value, e + )) })?; header_map.insert(name, value); } Ok(header_map) } - } #[cfg(test)] mod tests { + use super::*; use crate::AppError; use crate::{assert_error, assert_invalid_http_header, test}; use reqwest::ClientBuilder; - use super::*; test! { fn simple_get_request() { @@ -72,6 +82,25 @@ mod tests { } } + test! { + fn get_with_arbitrary_header() { + let url = Url::parse("https://example.com")?; + let actual = build_get_request(&url, vec![("X-Arbitrary".to_string(), "X-Arbitrary-Value".to_string())])?; + assert_eq!(actual.headers().get("X-Arbitrary").unwrap().to_str()?, "X-Arbitrary-Value"); + } + } + + test! { + fn post_request_with_text_body() { + let url = Url::parse("https://example.com")?; + let config = RequestConfig::new(Method::POST, url, vec![]); + let client = ClientBuilder::new().build()?; + let request = config.build(&client)?; + + assert_eq!(request.method(), Method::POST); + } + } + test! { fn invalid_header_name() { let url = Url::parse("https://example.com")?; @@ -93,5 +122,4 @@ mod tests { let client = ClientBuilder::new().build()?; config.build(&client) } - } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..083cf46 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +mod config; +mod dyndns_service; +mod error; +mod http; +mod ip_service; + +#[cfg(test)] +#[macro_use] +pub mod test_macros; + +pub use error::{AppError, AppResult}; diff --git a/src/main.rs b/src/main.rs index 2843eb3..3db2582 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,10 +12,11 @@ mod config; mod dyndns_service; mod error; mod ip_service; +mod network; +mod http; #[cfg(test)] mod test_macros; -mod http; #[derive(Parser, Debug)] #[command(ignore_errors(true), version, about, long_about = None)] diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..3697e29 --- /dev/null +++ b/src/network.rs @@ -0,0 +1,93 @@ +use crate::error::{AppError, AppResult}; +use network_interface::{NetworkInterface, NetworkInterfaceConfig}; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkAdapter { + pub name: String, + pub is_up: bool, + pub is_loopback: bool, +} + +impl fmt::Display for NetworkAdapter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl From for NetworkAdapter { + fn from(interface: NetworkInterface) -> Self { + let is_loopback = interface.name.to_lowercase().contains("lo"); + let is_up = !interface.addr.is_empty(); + + Self { + name: interface.name, + is_up, + is_loopback, + } + } +} + +pub fn get_network_adapters() -> AppResult> { + NetworkInterface::show() + .map_err(AppError::NetworkInterfaceError) + .map(|interfaces| interfaces.into_iter().map(NetworkAdapter::from).collect()) +} + +pub fn get_active_network_adapters() -> AppResult> { + get_network_adapters().map(|adapters| { + adapters + .into_iter() + .filter(|adapter| adapter.is_up && !adapter.is_loopback) + .collect() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test; + + test! { + fn test_get_network_adapters() { + let adapters = get_network_adapters()?; + + assert!(!adapters.is_empty(), "No network adapters found"); + + for adapter in &adapters { + assert!(!adapter.name.is_empty(), "Found adapter with empty name"); + } + + let up_adapters = adapters.iter().filter(|a| a.is_up).count(); + assert!(up_adapters > 0, "No adapters are up"); + + let non_loopback = adapters.iter().filter(|a| !a.is_loopback).count(); + assert!(non_loopback > 0, "All adapters are loopback interfaces"); + } + } + + test! { + fn test_get_active_network_adapters() { + let adapters = get_active_network_adapters()?; + + assert!(!adapters.is_empty(), "No active network adapters found"); + + for adapter in &adapters { + assert!(adapter.is_up, "Found inactive adapter: {}", adapter.name); + assert!(!adapter.is_loopback, "Found loopback adapter: {}", adapter.name); + } + } + } + + test! { + fn test_network_adapter_display() { + let adapter = NetworkAdapter { + name: "eth0".to_string(), + is_up: true, + is_loopback: false, + }; + + assert_eq!(format!("{}", adapter), "eth0"); + } + } +} -- cgit v1.2.3