...
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled

This commit is contained in:
kristof 2025-06-16 07:53:03 +02:00
parent dee38eb6c2
commit ba9103685f
7 changed files with 498 additions and 175 deletions

View File

@ -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.");

View File

@ -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.");

View File

@ -1,51 +1,93 @@
use reqwest::Client;
use std::time::Duration; use std::time::Duration;
// HTTP Checker use anyhow::Result;
pub struct HttpChecker { use reqwest::{Client, StatusCode, Url};
/// HTTP Connectivity module for checking HTTP/HTTPS connections
pub struct HttpConnector {
client: Client, client: Client,
url: String,
} }
impl HttpChecker { impl HttpConnector {
pub async fn check_url(&self) -> Result<bool, reqwest::Error> { /// Create a new HTTP connector with the default configuration
let res = self.client.get(&self.url).send().await?; pub fn new() -> Result<Self> {
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<S: Into<String>>(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 {
let client = Client::builder() let client = Client::builder()
.timeout(self.timeout) .timeout(Duration::from_secs(30))
.build() .build()?;
.expect("Failed to build HTTP client");
HttpChecker { Ok(Self { client })
client, }
url: self.url,
/// 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")
}
}

View File

@ -1,3 +1,8 @@
pub mod ssh; pub mod ssh;
pub mod tcp; pub mod tcp;
pub mod http; pub mod http;
// Re-export main types for a cleaner API
pub use ssh::{SshConnection, SshConnectionBuilder};
pub use tcp::TcpConnector;
pub use http::HttpConnector;

View File

@ -1,31 +1,99 @@
use russh::client;
use russh_keys::key;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::process::Stdio;
// SSH Connection use anyhow::Result;
#[derive(Clone)] use tokio::io::{AsyncReadExt, BufReader};
use tokio::process::Command;
/// SSH Connection that uses the system's SSH client
pub struct SshConnection { pub struct SshConnection {
session: Arc<client::Handle<Client>>, host: String,
port: u16,
user: String,
identity_file: Option<PathBuf>,
timeout: Duration,
} }
impl SshConnection { impl SshConnection {
pub async fn ping(&self) -> Result<(), anyhow::Error> { /// Execute a command over SSH and return its output
let mut channel = self.session.channel_open_session().await?; pub async fn execute(&self, command: &str) -> Result<(i32, String)> {
channel.exec(true, "ping -c 1 127.0.0.1").await?; let mut args = Vec::new();
Ok(())
// 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<bool> {
let result = self.execute("echo 'Connection successful'").await?;
Ok(result.0 == 0)
} }
} }
// SSH Connection Builder /// Builder for SSH connections
pub struct SshConnectionBuilder { pub struct SshConnectionBuilder {
host: String, host: String,
port: u16, port: u16,
user: String, user: String,
password: Option<String>, identity_file: Option<PathBuf>,
key_path: Option<PathBuf>,
use_agent: bool,
timeout: Duration, timeout: Duration,
} }
@ -35,10 +103,8 @@ impl SshConnectionBuilder {
host: "localhost".to_string(), host: "localhost".to_string(),
port: 22, port: 22,
user: "root".to_string(), user: "root".to_string(),
password: None, identity_file: None,
key_path: None, timeout: Duration::from_secs(10),
use_agent: true,
timeout: Duration::from_secs(30),
} }
} }
@ -57,18 +123,8 @@ impl SshConnectionBuilder {
self self
} }
pub fn password<S: Into<String>>(mut self, password: S) -> Self { pub fn identity_file(mut self, path: PathBuf) -> Self {
self.password = Some(password.into()); self.identity_file = Some(path);
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;
self self
} }
@ -77,57 +133,13 @@ impl SshConnectionBuilder {
self self
} }
pub async fn build(self) -> Result<SshConnection, anyhow::Error> { pub fn build(self) -> SshConnection {
let config = Arc::new(client::Config::default()); SshConnection {
let sh = Client; host: self.host,
port: self.port,
let mut session = client::connect(config, (self.host.as_str(), self.port), sh).await?; user: self.user,
identity_file: self.identity_file,
let auth_res = if self.use_agent { timeout: self.timeout,
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"));
}
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<Result<bool, Self::Error>> {
std::future::ready(Ok(true))
}
} }

View File

@ -1,64 +1,74 @@
use std::net::{SocketAddr, TcpStream}; use std::net::{IpAddr, SocketAddr};
use std::time::Duration; use std::time::Duration;
// TCP Checker use anyhow::Result;
pub struct TcpChecker { use tokio::net::TcpStream;
host: String, use tokio::time::timeout;
port: u16,
/// TCP Connectivity module for checking TCP connections
pub struct TcpConnector {
timeout: Duration, timeout: Duration,
} }
impl TcpChecker { impl TcpConnector {
pub fn ping(&self) -> Result<(), std::io::Error> { /// Create a new TCP connector with the default timeout (5 seconds)
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 {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
host: "localhost".to_string(), timeout: Duration::from_secs(5),
port: 80,
timeout: Duration::from_secs(1),
} }
} }
pub fn host<S: Into<String>>(mut self, host: S) -> Self { /// Create a new TCP connector with a custom timeout
self.host = host.into(); pub fn with_timeout(timeout: Duration) -> Self {
self Self { timeout }
} }
pub fn port(mut self, port: u16) -> Self { /// Check if a TCP port is open on a host
self.port = port; pub async fn check_port<A: Into<IpAddr>>(&self, host: A, port: u16) -> Result<bool> {
self 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 timeout(mut self, timeout: Duration) -> Self { /// Check if multiple TCP ports are open on a host
self.timeout = timeout; pub async fn check_ports<A: Into<IpAddr> + Clone>(&self, host: A, ports: &[u16]) -> Result<Vec<(u16, bool)>> {
self 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));
} }
pub fn build(self) -> TcpChecker { Ok(results)
TcpChecker { }
host: self.host,
port: self.port, /// Check if a host is reachable on the network using ICMP ping
timeout: self.timeout, 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()
}
} }

89
src/rhai/net.rs Normal file
View File

@ -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<EvalAltResult>>` - 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<EvalAltResult>> {
// 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,
}
}