feat: Add sal-net package to workspace
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
- Add new sal-net package to the workspace. - Update MONOREPO_CONVERSION_PLAN.md to reflect the addition of the sal-net package and mark it as production-ready. - Add Cargo.toml and README.md for the sal-net package.
This commit is contained in:
84
net/src/http.rs
Normal file
84
net/src/http.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
|
||||
/// HTTP Connectivity module for checking HTTP/HTTPS connections
|
||||
pub struct HttpConnector {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl HttpConnector {
|
||||
/// Create a new HTTP connector with the default configuration
|
||||
pub fn new() -> Result<Self> {
|
||||
let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Create a new HTTP connector with a custom timeout
|
||||
pub fn with_timeout(timeout: Duration) -> Result<Self> {
|
||||
let client = Client::builder().timeout(timeout).build()?;
|
||||
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Check if a URL is reachable
|
||||
pub async fn check_url<U: AsRef<str>>(&self, url: U) -> Result<bool> {
|
||||
let url_str = url.as_ref();
|
||||
let url = Url::parse(url_str)?;
|
||||
|
||||
let result = self.client.head(url).send().await;
|
||||
|
||||
Ok(result.is_ok())
|
||||
}
|
||||
|
||||
/// Check a URL and return the status code if reachable
|
||||
pub async fn check_status<U: AsRef<str>>(&self, url: U) -> Result<Option<StatusCode>> {
|
||||
let url_str = url.as_ref();
|
||||
let url = Url::parse(url_str)?;
|
||||
|
||||
let result = self.client.head(url).send().await;
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(Some(response.status())),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the content of a URL
|
||||
pub async fn get_content<U: AsRef<str>>(&self, url: U) -> Result<String> {
|
||||
let url_str = url.as_ref();
|
||||
let url = Url::parse(url_str)?;
|
||||
|
||||
let response = self.client.get(url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"HTTP request failed with status: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let content = response.text().await?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Verify that a URL responds with a specific status code
|
||||
pub async fn verify_status<U: AsRef<str>>(
|
||||
&self,
|
||||
url: U,
|
||||
expected_status: StatusCode,
|
||||
) -> Result<bool> {
|
||||
match self.check_status(url).await? {
|
||||
Some(status) => Ok(status == expected_status),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HttpConnector {
|
||||
fn default() -> Self {
|
||||
Self::new().expect("Failed to create default HttpConnector")
|
||||
}
|
||||
}
|
9
net/src/lib.rs
Normal file
9
net/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod http;
|
||||
pub mod rhai;
|
||||
pub mod ssh;
|
||||
pub mod tcp;
|
||||
|
||||
// Re-export main types for a cleaner API
|
||||
pub use http::HttpConnector;
|
||||
pub use ssh::{SshConnection, SshConnectionBuilder};
|
||||
pub use tcp::TcpConnector;
|
180
net/src/rhai.rs
Normal file
180
net/src/rhai.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! Rhai wrappers for network module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for network connectivity functions.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Module};
|
||||
|
||||
/// Create a Rhai module with network functions
|
||||
pub fn create_module() -> Module {
|
||||
// For now, we'll use a simpler approach and register functions via engine
|
||||
// This ensures compatibility with Rhai's type system
|
||||
// The module is created but functions are registered through register_net_module
|
||||
|
||||
Module::new()
|
||||
}
|
||||
|
||||
/// Register network module functions with the Rhai engine
|
||||
pub fn register_net_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// TCP functions
|
||||
engine.register_fn("tcp_check", tcp_check);
|
||||
engine.register_fn("tcp_ping", tcp_ping);
|
||||
|
||||
// HTTP functions
|
||||
engine.register_fn("http_check", http_check);
|
||||
engine.register_fn("http_status", http_status);
|
||||
|
||||
// SSH functions
|
||||
engine.register_fn("ssh_execute", ssh_execute);
|
||||
engine.register_fn("ssh_execute_output", ssh_execute_output);
|
||||
engine.register_fn("ssh_ping", ssh_ping_host);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a TCP port is open
|
||||
pub fn tcp_check(host: &str, port: i64) -> bool {
|
||||
// Use std::net::TcpStream for synchronous connection test
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
// Parse the address
|
||||
let addr_str = format!("{}:{}", host, port);
|
||||
if let Ok(socket_addr) = addr_str.parse::<SocketAddr>() {
|
||||
// Try to connect with a timeout
|
||||
TcpStream::connect_timeout(&socket_addr, Duration::from_secs(5)).is_ok()
|
||||
} else {
|
||||
// Try to resolve hostname first
|
||||
match std::net::ToSocketAddrs::to_socket_addrs(&addr_str) {
|
||||
Ok(mut addrs) => {
|
||||
if let Some(addr) = addrs.next() {
|
||||
TcpStream::connect_timeout(&addr, Duration::from_secs(5)).is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ping a host using ICMP (cross-platform)
|
||||
pub fn tcp_ping(host: &str) -> bool {
|
||||
// Use system ping command for synchronous operation
|
||||
use std::process::Command;
|
||||
|
||||
// Cross-platform ping implementation
|
||||
let mut cmd = Command::new("ping");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
cmd.arg("-n").arg("1").arg("-w").arg("5000"); // Windows: -n count, -w timeout in ms
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
cmd.arg("-c").arg("1").arg("-W").arg("5"); // Unix: -c count, -W timeout in seconds
|
||||
}
|
||||
|
||||
cmd.arg(host);
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => output.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an HTTP URL is reachable
|
||||
pub fn http_check(url: &str) -> bool {
|
||||
use std::time::Duration;
|
||||
|
||||
// Create a blocking HTTP client with timeout
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Try to make a HEAD request
|
||||
match client.head(url).send() {
|
||||
Ok(response) => response.status().is_success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get HTTP status code from a URL
|
||||
pub fn http_status(url: &str) -> i64 {
|
||||
use std::time::Duration;
|
||||
|
||||
// Create a blocking HTTP client with timeout
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
// Try to make a HEAD request
|
||||
match client.head(url).send() {
|
||||
Ok(response) => response.status().as_u16() as i64,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command via SSH - returns exit code as i64
|
||||
pub fn ssh_execute(host: &str, user: &str, command: &str) -> i64 {
|
||||
use std::process::Command;
|
||||
|
||||
let mut cmd = Command::new("ssh");
|
||||
cmd.arg("-o")
|
||||
.arg("ConnectTimeout=5")
|
||||
.arg("-o")
|
||||
.arg("StrictHostKeyChecking=no")
|
||||
.arg(format!("{}@{}", user, host))
|
||||
.arg(command);
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => output.status.code().unwrap_or(-1) as i64,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command via SSH and get output - returns output as string
|
||||
pub fn ssh_execute_output(host: &str, user: &str, command: &str) -> String {
|
||||
use std::process::Command;
|
||||
|
||||
let mut cmd = Command::new("ssh");
|
||||
cmd.arg("-o")
|
||||
.arg("ConnectTimeout=5")
|
||||
.arg("-o")
|
||||
.arg("StrictHostKeyChecking=no")
|
||||
.arg(format!("{}@{}", user, host))
|
||||
.arg(command);
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
Err(_) => "SSH command failed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test SSH connectivity to a host
|
||||
pub fn ssh_ping_host(host: &str, user: &str) -> bool {
|
||||
use std::process::Command;
|
||||
|
||||
let mut cmd = Command::new("ssh");
|
||||
cmd.arg("-o")
|
||||
.arg("ConnectTimeout=5")
|
||||
.arg("-o")
|
||||
.arg("StrictHostKeyChecking=no")
|
||||
.arg("-o")
|
||||
.arg("BatchMode=yes") // Non-interactive
|
||||
.arg(format!("{}@{}", user, host))
|
||||
.arg("echo 'Connection successful'");
|
||||
|
||||
match cmd.output() {
|
||||
Ok(output) => output.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
151
net/src/ssh.rs
Normal file
151
net/src/ssh.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// SSH Connection that uses the system's SSH client
|
||||
pub struct SshConnection {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
identity_file: Option<PathBuf>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl SshConnection {
|
||||
/// Execute a command over SSH and return its output
|
||||
pub async fn execute(&self, command: &str) -> Result<(i32, String)> {
|
||||
let mut args = Vec::new();
|
||||
|
||||
// Add SSH options
|
||||
args.push("-o".to_string());
|
||||
args.push(format!("ConnectTimeout={}", self.timeout.as_secs()));
|
||||
|
||||
// Don't check host key to avoid prompts
|
||||
args.push("-o".to_string());
|
||||
args.push("StrictHostKeyChecking=no".to_string());
|
||||
|
||||
// Specify port if not default
|
||||
if self.port != 22 {
|
||||
args.push("-p".to_string());
|
||||
args.push(self.port.to_string());
|
||||
}
|
||||
|
||||
// Add identity file if provided
|
||||
if let Some(identity) = &self.identity_file {
|
||||
args.push("-i".to_string());
|
||||
args.push(identity.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Add user and host
|
||||
args.push(format!("{}@{}", self.user, self.host));
|
||||
|
||||
// Add the command to execute
|
||||
args.push(command.to_string());
|
||||
|
||||
// Run the SSH command
|
||||
let mut child = Command::new("ssh")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
// Collect stdout and stderr
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
|
||||
let mut stdout_reader = BufReader::new(stdout);
|
||||
let mut stderr_reader = BufReader::new(stderr);
|
||||
|
||||
let mut output = String::new();
|
||||
stdout_reader.read_to_string(&mut output).await?;
|
||||
|
||||
let mut error_output = String::new();
|
||||
stderr_reader.read_to_string(&mut error_output).await?;
|
||||
|
||||
// If there's error output, append it to the regular output
|
||||
if !error_output.is_empty() {
|
||||
if !output.is_empty() {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&error_output);
|
||||
}
|
||||
|
||||
// Wait for the command to complete and get exit status
|
||||
let status = child.wait().await?;
|
||||
let code = status.code().unwrap_or(-1);
|
||||
|
||||
Ok((code, output))
|
||||
}
|
||||
|
||||
/// Check if the host is reachable via SSH
|
||||
pub async fn ping(&self) -> Result<bool> {
|
||||
let result = self.execute("echo 'Connection successful'").await?;
|
||||
Ok(result.0 == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SSH connections
|
||||
pub struct SshConnectionBuilder {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
identity_file: Option<PathBuf>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for SshConnectionBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshConnectionBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
host: "localhost".to_string(),
|
||||
port: 22,
|
||||
user: "root".to_string(),
|
||||
identity_file: None,
|
||||
timeout: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host<S: Into<String>>(mut self, host: S) -> Self {
|
||||
self.host = host.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user<S: Into<String>>(mut self, user: S) -> Self {
|
||||
self.user = user.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn identity_file(mut self, path: PathBuf) -> Self {
|
||||
self.identity_file = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> SshConnection {
|
||||
SshConnection {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
user: self.user,
|
||||
identity_file: self.identity_file,
|
||||
timeout: self.timeout,
|
||||
}
|
||||
}
|
||||
}
|
78
net/src/tcp.rs
Normal file
78
net/src/tcp.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// TCP Connectivity module for checking TCP connections
|
||||
pub struct TcpConnector {
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl TcpConnector {
|
||||
/// Create a new TCP connector with the default timeout (5 seconds)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new TCP connector with a custom timeout
|
||||
pub fn with_timeout(timeout: Duration) -> Self {
|
||||
Self { timeout }
|
||||
}
|
||||
|
||||
/// Check if a TCP port is open on a host
|
||||
pub async fn check_port<A: Into<IpAddr>>(&self, host: A, port: u16) -> Result<bool> {
|
||||
let addr = SocketAddr::new(host.into(), port);
|
||||
let connect_future = TcpStream::connect(addr);
|
||||
|
||||
match timeout(self.timeout, connect_future).await {
|
||||
Ok(Ok(_)) => Ok(true),
|
||||
Ok(Err(_)) => Ok(false),
|
||||
Err(_) => Ok(false), // Timeout occurred
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if multiple TCP ports are open on a host
|
||||
pub async fn check_ports<A: Into<IpAddr> + Clone>(
|
||||
&self,
|
||||
host: A,
|
||||
ports: &[u16],
|
||||
) -> Result<Vec<(u16, bool)>> {
|
||||
let mut results = Vec::with_capacity(ports.len());
|
||||
|
||||
for &port in ports {
|
||||
let is_open = self.check_port(host.clone(), port).await?;
|
||||
results.push((port, is_open));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Check if a host is reachable on the network using ICMP ping
|
||||
pub async fn ping<S: AsRef<str>>(&self, host: S) -> Result<bool> {
|
||||
// Convert to owned strings to avoid borrowing issues
|
||||
let host_str = host.as_ref().to_string();
|
||||
let timeout_secs = self.timeout.as_secs().to_string();
|
||||
|
||||
// Run the ping command with explicit arguments
|
||||
let status = tokio::process::Command::new("ping")
|
||||
.arg("-c")
|
||||
.arg("1") // Just one ping
|
||||
.arg("-W")
|
||||
.arg(timeout_secs) // Timeout in seconds
|
||||
.arg(host_str) // Host to ping
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
Ok(status.status.success())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TcpConnector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user