diff --git a/examples/network/network_connectivity.rhai b/examples/network/network_connectivity.rhai new file mode 100644 index 0000000..9ef0dc8 --- /dev/null +++ b/examples/network/network_connectivity.rhai @@ -0,0 +1,83 @@ +// Example of using the network modules in SAL +// Shows TCP port checking, HTTP URL validation, and SSH command execution + +// Import system module for display +import "os" as os; + +// Function to print section header +fn section(title) { + print("\n"); + print("==== " + title + " ===="); + print("\n"); +} + +// TCP connectivity checks +section("TCP Connectivity"); + +// Create a TCP connector +let tcp = sal::net::TcpConnector::new(); + +// Check if a port is open +let host = "localhost"; +let port = 22; +print(`Checking if port ${port} is open on ${host}...`); +let is_open = tcp.check_port(host, port); +print(`Port ${port} is ${is_open ? "open" : "closed"}`); + +// Check multiple ports +let ports = [22, 80, 443]; +print(`Checking multiple ports on ${host}...`); +let port_results = tcp.check_ports(host, ports); +for result in port_results { + print(`Port ${result.0} is ${result.1 ? "open" : "closed"}`); +} + +// HTTP connectivity checks +section("HTTP Connectivity"); + +// Create an HTTP connector +let http = sal::net::HttpConnector::new(); + +// Check if a URL is reachable +let url = "https://www.example.com"; +print(`Checking if ${url} is reachable...`); +let is_reachable = http.check_url(url); +print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`); + +// Check the status code of a URL +print(`Checking status code of ${url}...`); +let status = http.check_status(url); +if status { + print(`Status code: ${status.unwrap()}`); +} else { + print("Failed to get status code"); +} + +// Only attempt SSH if port 22 is open +if is_open { + // SSH connectivity checks + section("SSH Connectivity"); + + // Create an SSH connection to localhost (if SSH server is running) + print("Attempting to connect to SSH server on localhost..."); + + // Using the builder pattern + let ssh = sal::net::SshConnectionBuilder::new() + .host("localhost") + .port(22) + .user(os::get_env("USER") || "root") + .build(); + + // Execute a simple command + print("Executing 'uname -a' command..."); + let result = ssh.execute("uname -a"); + if result.0 == 0 { + print("Command output:"); + print(result.1); + } else { + print(`Command failed with exit code: ${result.0}`); + print(result.1); + } +} + +print("\nNetwork connectivity checks completed."); \ No newline at end of file diff --git a/examples/network/network_rhai.rhai b/examples/network/network_rhai.rhai new file mode 100644 index 0000000..0178f4c --- /dev/null +++ b/examples/network/network_rhai.rhai @@ -0,0 +1,82 @@ +// Example of using the network modules in SAL through Rhai +// Shows TCP port checking, HTTP URL validation, and SSH command execution + +// Function to print section header +fn section(title) { + print("\n"); + print("==== " + title + " ===="); + print("\n"); +} + +// TCP connectivity checks +section("TCP Connectivity"); + +// Create a TCP connector +let tcp = net::new_tcp_connector(); + +// Check if a port is open +let host = "localhost"; +let port = 22; +print(`Checking if port ${port} is open on ${host}...`); +let is_open = tcp.check_port(host, port); +print(`Port ${port} is ${is_open ? "open" : "closed"}`); + +// Check multiple ports +let ports = [22, 80, 443]; +print(`Checking multiple ports on ${host}...`); +let port_results = tcp.check_ports(host, ports); +for result in port_results { + print(`Port ${result.port} is ${result.is_open ? "open" : "closed"}`); +} + +// HTTP connectivity checks +section("HTTP Connectivity"); + +// Create an HTTP connector +let http = net::new_http_connector(); + +// Check if a URL is reachable +let url = "https://www.example.com"; +print(`Checking if ${url} is reachable...`); +let is_reachable = http.check_url(url); +print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`); + +// Check the status code of a URL +print(`Checking status code of ${url}...`); +let status = http.check_status(url); +if status != () { + print(`Status code: ${status}`); +} else { + print("Failed to get status code"); +} + +// Get content from a URL +print(`Getting content from ${url}...`); +let content = http.get_content(url); +print(`Content length: ${content.len()} characters`); +print(`First 100 characters: ${content.substr(0, 100)}...`); + +// Only attempt SSH if port 22 is open +if is_open { + // SSH connectivity checks + section("SSH Connectivity"); + + // Create an SSH connection to localhost (if SSH server is running) + print("Attempting to connect to SSH server on localhost..."); + + // Using the builder pattern + let ssh = net::new_ssh_builder() + .host("localhost") + .port(22) + .user(os::get_env("USER") || "root") + .timeout(10) + .build(); + + // Execute a simple command + print("Executing 'uname -a' command..."); + let result = ssh.execute("uname -a"); + print(`Command exit code: ${result.code}`); + print(`Command output: ${result.output}`); +} + +print("\nNetwork connectivity checks completed."); \ No newline at end of file diff --git a/src/net/http.rs b/src/net/http.rs index 746b6fd..da85467 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -1,51 +1,93 @@ -use reqwest::Client; use std::time::Duration; -// HTTP Checker -pub struct HttpChecker { +use anyhow::Result; +use reqwest::{Client, StatusCode, Url}; + +/// HTTP Connectivity module for checking HTTP/HTTPS connections +pub struct HttpConnector { client: Client, - url: String, } -impl HttpChecker { - pub async fn check_url(&self) -> Result { - let res = self.client.get(&self.url).send().await?; - Ok(res.status().is_success()) - } -} - -// HTTP Checker Builder -pub struct HttpCheckerBuilder { - url: String, - timeout: Duration, -} - -impl HttpCheckerBuilder { - pub fn new() -> Self { - Self { - url: "http://localhost".to_string(), - timeout: Duration::from_secs(30), - } - } - - pub fn url>(mut self, url: S) -> Self { - self.url = url.into(); - self - } - - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self - } - - pub fn build(self) -> HttpChecker { +impl HttpConnector { + /// Create a new HTTP connector with the default configuration + pub fn new() -> Result { let client = Client::builder() - .timeout(self.timeout) - .build() - .expect("Failed to build HTTP client"); - HttpChecker { - client, - url: self.url, + .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 { + let client = Client::builder() + .timeout(timeout) + .build()?; + + Ok(Self { client }) + } + + /// Check if a URL is reachable + pub async fn check_url>(&self, url: U) -> Result { + 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>(&self, url: U) -> Result> { + 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>(&self, url: U) -> Result { + 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>(&self, url: U, expected_status: StatusCode) -> Result { + 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") + } } \ No newline at end of file diff --git a/src/net/mod.rs b/src/net/mod.rs index 6ed6308..6bb9ad2 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -1,3 +1,8 @@ pub mod ssh; pub mod tcp; -pub mod http; \ No newline at end of file +pub mod http; + +// Re-export main types for a cleaner API +pub use ssh::{SshConnection, SshConnectionBuilder}; +pub use tcp::TcpConnector; +pub use http::HttpConnector; \ No newline at end of file diff --git a/src/net/ssh.rs b/src/net/ssh.rs index 8dfc738..42795bc 100644 --- a/src/net/ssh.rs +++ b/src/net/ssh.rs @@ -1,31 +1,99 @@ -use russh::client; -use russh_keys::key; use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; +use std::process::Stdio; -// SSH Connection -#[derive(Clone)] +use anyhow::Result; +use tokio::io::{AsyncReadExt, BufReader}; +use tokio::process::Command; + +/// SSH Connection that uses the system's SSH client pub struct SshConnection { - session: Arc>, + host: String, + port: u16, + user: String, + identity_file: Option, + timeout: Duration, } impl SshConnection { - pub async fn ping(&self) -> Result<(), anyhow::Error> { - let mut channel = self.session.channel_open_session().await?; - channel.exec(true, "ping -c 1 127.0.0.1").await?; - Ok(()) + /// 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_str("\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 { + let result = self.execute("echo 'Connection successful'").await?; + Ok(result.0 == 0) } } -// SSH Connection Builder +/// Builder for SSH connections pub struct SshConnectionBuilder { host: String, port: u16, user: String, - password: Option, - key_path: Option, - use_agent: bool, + identity_file: Option, timeout: Duration, } @@ -35,10 +103,8 @@ impl SshConnectionBuilder { host: "localhost".to_string(), port: 22, user: "root".to_string(), - password: None, - key_path: None, - use_agent: true, - timeout: Duration::from_secs(30), + identity_file: None, + timeout: Duration::from_secs(10), } } @@ -57,18 +123,8 @@ impl SshConnectionBuilder { self } - pub fn password>(mut self, password: S) -> Self { - self.password = Some(password.into()); - self - } - - pub fn key_path(mut self, key_path: PathBuf) -> Self { - self.key_path = Some(key_path); - self - } - - pub fn use_agent(mut self, use_agent: bool) -> Self { - self.use_agent = use_agent; + pub fn identity_file(mut self, path: PathBuf) -> Self { + self.identity_file = Some(path); self } @@ -77,57 +133,13 @@ impl SshConnectionBuilder { self } - pub async fn build(self) -> Result { - let config = Arc::new(client::Config::default()); - let sh = Client; - - let mut session = client::connect(config, (self.host.as_str(), self.port), sh).await?; - - let auth_res = if self.use_agent { - let mut agent = russh_keys::agent::client::AgentClient::connect_env().await?; - let mut keys = agent.request_identities().await?; - if keys.is_empty() { - return Err(anyhow::anyhow!("No identities found in ssh-agent")); - } - let key = keys.remove(0); - let (_agent, authed) = session - .authenticate_future(self.user.as_str(), Arc::new(key), agent) - .await; - authed? - } else if let Some(password) = self.password { - session - .authenticate_password(self.user.as_str(), &password) - .await? - } else if let Some(key_path) = self.key_path { - let key_pair = russh_keys::load_secret_key(key_path, None)?; - session - .authenticate_publickey(self.user.as_str(), Arc::new(key_pair)) - .await? - } else { - return Err(anyhow::anyhow!( - "No authentication method specified" - )); - }; - - if !auth_res { - return Err(anyhow::anyhow!("Authentication failed")); + pub fn build(self) -> SshConnection { + SshConnection { + host: self.host, + port: self.port, + user: self.user, + identity_file: self.identity_file, + timeout: self.timeout, } - - Ok(SshConnection { - session: Arc::new(session), - }) - } -} - -struct Client; - -impl client::Handler for Client { - type Error = russh::Error; - - fn check_server_key( - &mut self, - _server_public_key: &key::PublicKey, - ) -> std::future::Ready> { - std::future::ready(Ok(true)) } } \ No newline at end of file diff --git a/src/net/tcp.rs b/src/net/tcp.rs index 0082654..e5cc9f6 100644 --- a/src/net/tcp.rs +++ b/src/net/tcp.rs @@ -1,64 +1,74 @@ -use std::net::{SocketAddr, TcpStream}; +use std::net::{IpAddr, SocketAddr}; use std::time::Duration; -// TCP Checker -pub struct TcpChecker { - host: String, - port: u16, +use anyhow::Result; +use tokio::net::TcpStream; +use tokio::time::timeout; + +/// TCP Connectivity module for checking TCP connections +pub struct TcpConnector { timeout: Duration, } -impl TcpChecker { - pub fn ping(&self) -> Result<(), std::io::Error> { - let addr = format!("{}:{}", self.host, self.port); - let socket_addr: SocketAddr = addr.parse().expect("Failed to parse socket address"); - TcpStream::connect_timeout(&socket_addr, self.timeout)?; - Ok(()) - } - - pub fn check_port(&self) -> bool { - let addr = format!("{}:{}", self.host, self.port); - let socket_addr: SocketAddr = addr.parse().expect("Failed to parse socket address"); - TcpStream::connect_timeout(&socket_addr, self.timeout).is_ok() - } -} - -// TCP Checker Builder -pub struct TcpCheckerBuilder { - host: String, - port: u16, - timeout: Duration, -} - -impl TcpCheckerBuilder { +impl TcpConnector { + /// Create a new TCP connector with the default timeout (5 seconds) pub fn new() -> Self { Self { - host: "localhost".to_string(), - port: 80, - timeout: Duration::from_secs(1), + 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>(&self, host: A, port: u16) -> Result { + 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 } } - pub fn host>(mut self, host: S) -> Self { - self.host = host.into(); - self - } - - pub fn port(mut self, port: u16) -> Self { - self.port = port; - self - } - - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self - } - - pub fn build(self) -> TcpChecker { - TcpChecker { - host: self.host, - port: self.port, - timeout: self.timeout, + /// Check if multiple TCP ports are open on a host + pub async fn check_ports + Clone>(&self, host: A, ports: &[u16]) -> Result> { + 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>(&self, host: S) -> Result { + // 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() } } \ No newline at end of file diff --git a/src/rhai/net.rs b/src/rhai/net.rs new file mode 100644 index 0000000..55377f2 --- /dev/null +++ b/src/rhai/net.rs @@ -0,0 +1,89 @@ +//! Rhai wrappers for network module functions +//! +//! This module provides Rhai wrappers for network connectivity functions. + +use rhai::{Engine, EvalAltResult}; +use crate::net::TcpConnector; +use super::error::register_error_types; + +/// Register network module functions with the Rhai engine +/// +/// # Arguments +/// +/// * `engine` - The Rhai engine to register the functions with +/// +/// # Returns +/// +/// * `Result<(), Box>` - Ok if registration was successful, Err otherwise +pub fn create_module() -> rhai::Module { + let mut module = rhai::Module::new(); + + // Register basic TCP functions + module.set_native_fn("tcp_check", tcp_check); + module.set_native_fn("tcp_ping", tcp_ping); + + module +} + +/// Register network module functions with the Rhai engine +pub fn register_net_module(engine: &mut Engine) -> Result<(), Box> { + // Register error types + register_error_types(engine)?; + + // TCP functions + engine.register_fn("tcp_check", tcp_check); + engine.register_fn("tcp_ping", tcp_ping); + + Ok(()) +} + +/// Check if a TCP port is open +pub fn tcp_check(host: &str, port: i64) -> bool { + let connector = TcpConnector::new(); + + // Create a simple runtime to run the async function + match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() { + Ok(rt) => { + rt.block_on(async { + // Resolve host name first + let sock_addr = format!("{}:{}", host, port); + match tokio::net::lookup_host(sock_addr).await { + Ok(mut addrs) => { + if let Some(addr) = addrs.next() { + match connector.check_port(addr.ip(), port as u16).await { + Ok(is_open) => is_open, + Err(_) => false, + } + } else { + false + } + }, + Err(_) => false, + } + }) + }, + Err(_) => false, + } +} + +/// Ping a host using ICMP +pub fn tcp_ping(host: &str) -> bool { + let connector = TcpConnector::new(); + + // Create a simple runtime to run the async function + match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() { + Ok(rt) => { + rt.block_on(async { + match connector.ping(host).await { + Ok(result) => result, + Err(_) => false, + } + }) + }, + Err(_) => false, + } +} \ No newline at end of file