connchk/
lib.rs

1/*   
2    connchk gives a status of reachability of plain tcp or http(s) endpoints from your machine
3    Copyright (C) 2020-2024 Anthony Martinez
4
5    Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
6    http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
7    http://opensource.org/licenses/MIT>, at your option. This file may not be
8    copied, modified, or distributed except according to those terms.
9*/
10
11//!
12//! `connchk` is command-line network checking tool written in Rust. It aims
13//! to provide a cross platform utility that can verify if your host can reach
14//! targets defined in a TOML document. Using the library a user can incorporate
15//! network checks into independent works.
16
17use std::boxed::Box;
18use std::collections::HashMap;
19use std::net::{Shutdown, TcpStream};
20use std::path::PathBuf;
21use std::time::Instant;
22
23use clap::{Command, Arg, crate_authors, crate_version, crate_description};
24use rayon::prelude::*;
25use reqwest::StatusCode;
26use reqwest::blocking::{Client, Response};
27use serde::Deserialize;
28use serde_json::Value;
29
30
31/// Provides argument handling using Clap
32pub fn arg_handler() -> Option<PathBuf> {
33    let matches = Command::new("connchk")
34        .version(crate_version!())
35        .author(crate_authors!())
36	.about(crate_description!())
37        .arg(Arg::new("config")
38             .help("Path to the configuration file to use")
39             .index(1)
40             .required(true))
41        .get_matches();
42	
43    matches.get_one::<String>("config").map(PathBuf::from)
44}
45
46/// Provides a deserialize target for optional parameters in
47/// custom HTTP(s) checks.
48#[derive(Deserialize, Debug, Clone)]
49pub struct HttpOptions {
50    pub params: Option<HashMap<String,String>>,
51    pub json: Option<Value>,
52    pub ok: u16,
53}
54
55/// A generic resource combining all possible fields into a common type
56#[derive(Deserialize, Debug)]
57pub struct Resource {
58    pub desc: String,
59    pub addr: String,
60    pub custom: Option<HttpOptions>,
61    pub kind: ResType,
62    pub res: Option<String>,
63}
64
65impl Resource {
66    /// Executes connectivity checks for each type defined in [`ResType`]
67    pub fn check(&self) -> Result<(), Box<dyn std::error::Error>> {
68	match self.kind {
69	    ResType::Tcp => {
70		self.check_tcp()?;
71	    },
72	    ResType::Http => {
73		if let Some(opts) = &self.custom {
74		    self.check_http_custom(opts)?;
75		} else {
76		    self.check_http_basic()?;
77		}
78	    }
79	}
80	Ok(())
81    }
82
83    /// Checks an HTTP(s) endpoint's availability with a GET request.
84    /// Prints a success message if the status code is 200 OK, or
85    /// failure details in any other case.
86    fn check_http_basic(&self) -> Result<(), Box<dyn std::error::Error>> {
87	let client = Client::new();
88	let resp = client.get(&self.addr).send()?;
89	if resp.status() == StatusCode::OK {
90	    Ok(())
91	} else {
92	    let msg = format!("\n\tStatus: {}\n\tDetails: {}", resp.status().as_str(), resp.text()?);
93	    Err(From::from(msg))
94	}
95    }
96
97    /// Checks an HTTP(s) endpoint's availability with a form POST request.
98    /// Values are defined in the `HttpOptions` struct.
99    /// Prints a success message if the status code is equal to the `ok` value,
100    /// or failure details when the status code is equaly to the `bad` value or
101    /// any other value/error.
102    fn check_http_custom(&self, options: &HttpOptions) -> Result<(), Box<dyn std::error::Error>> {
103	let client = Client::new();
104	let resp: Response;
105	if let Some(params) = &options.params {
106	    resp = client.post(&self.addr)
107		.form(params)
108		.send()?;
109	    self.custom_http_resp(options, resp)?
110	} else if let Some(json) = &options.json {
111	    resp = client.post(&self.addr)
112		.json(json)
113		.send()?;
114	    self.custom_http_resp(options, resp)?
115	};
116
117	Ok(())
118    }
119
120    /// Returns the response details for HTTP(s) checks when the [`HttpResource.custom`] field
121    /// is used. 
122    fn custom_http_resp(&self, options: &HttpOptions, resp: Response) -> Result<(), Box<dyn std::error::Error>> {
123	let resp_code = resp.status().as_u16();
124	if resp_code == options.ok {
125	    Ok(())
126	} else {
127	    let msg = format!("\n\tStatus: {}\n\tDetails: {}", resp.status().as_str(), resp.text()?);
128	    Err(From::from(msg))
129	}
130    }
131
132    /// Checks a TCP endpoint's availability with by establishing a [`TcpStream`]
133    /// Prints a success message if the stream opens without error, or returns
134    /// failure details in any other case.
135    fn check_tcp(&self) -> Result<(), Box<dyn std::error::Error>> {
136	let stream = TcpStream::connect(&self.addr)?;
137	stream.shutdown(Shutdown::Both)?;
138	Ok(())
139    }
140}
141
142/// Classifies the resource type for the top-level [`Resource`] struct
143#[derive(Deserialize, Debug)]
144pub enum ResType {
145    /// An HTTP(s) resource
146    Http,
147    /// A TCP resource
148    Tcp,
149}
150
151/// Provides a deserialize target for TOML configuration files
152/// defining multiple [`Resource`] entities
153#[derive(Deserialize, Debug)]
154pub struct NetworkResources {
155    pub target: Vec<Resource>,
156}
157
158impl NetworkResources {
159    /// Executes parallel connectivity checks for all [`Resource`]
160    /// objects contained within the higher level [`NetworkResources`]
161    /// struct. Prints success message with call latency or failure message
162    /// with available details. Maintains the resource order defined in the
163    /// supplied TOML configuration file.
164    pub fn check_resources(&mut self) {
165	self.target.par_iter_mut()
166	    .for_each(|el| {
167		let now = Instant::now();
168		match el.check() {
169		    Ok(_) => {
170			let dur = now.elapsed().as_millis();
171			let res = format!("Successfully connected to {} in {}ms", el.desc, dur);
172			el.res = Some(res);
173		    },
174		    Err(e) => {
175			let res = format!("Failed to connect to {} with: {}", el.desc, e);
176			el.res = Some(res);
177		    }
178		}
179	    });
180
181	for target in self.target.iter() {
182	    if let Some(result) = &target.res {
183		println!("{}", result)
184	    }
185	}
186    }
187}