pub use isoloom_spec::probe::{Host, Url, can_run_checks};
mod derived;
mod runner;
pub use derived::{allowed, derived, from_networks};
pub use runner::{Line, Render, parse_line, script};
use isoloom_spec::model::{Check, Declared, Expect, Spec};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Position {
Machine(String),
Networks,
}
impl Position {
#[must_use]
pub fn id(&self) -> &str {
match self {
Position::Machine(m) => m,
Position::Networks => "networks",
}
}
#[must_use]
pub fn label(&self) -> String {
match self {
Position::Machine(m) => format!("from {m}"),
Position::Networks => "from the environment's networks".to_owned(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpExpect {
Status(u16),
Any,
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TcpExpect {
Open,
Blocked,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HttpRequest {
pub method: Option<String>,
pub headers: Vec<(String, String)>,
pub body: Option<String>,
pub contains: Option<String>,
pub follow: bool,
}
impl HttpRequest {
fn is_plain(&self) -> bool {
self.method.is_none() && self.headers.is_empty() && self.body.is_none() && self.contains.is_none()
}
}
pub const HTTP_MAX_REDIRECTS: u32 = 5;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TcpExchange {
pub send: Option<String>,
pub contains: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Probe {
Http {
url: Url,
expect: HttpExpect,
request: HttpRequest,
},
Tcp {
host: Host,
port: u16,
expect: TcpExpect,
exchange: TcpExchange,
},
Exec {
command: String,
expect: Option<String>,
},
Script {
path: String,
},
Playbook {
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub name: String,
pub position: Position,
pub probe: Probe,
pub wait: u32,
pub derived: bool,
}
#[must_use]
pub fn default_position(spec: &Spec) -> Position {
match spec.machines.iter().find(|(_, m)| m.access) {
Some((name, _)) => Position::Machine(name.clone()),
None => Position::Networks,
}
}
#[must_use]
pub fn plan(spec: &Spec) -> Vec<Resolved> {
let default = default_position(spec);
let mut out: Vec<Resolved> = spec
.checks
.iter()
.filter_map(|c| match c {
Check::Script(p) => Some(Resolved {
name: p.clone(),
position: default.clone(),
probe: if c.is_playbook() {
Probe::Playbook { path: p.clone() }
} else {
Probe::Script { path: p.clone() }
},
wait: 0,
derived: false,
}),
Check::Declared(d) => declared(d, &default),
})
.collect();
out.extend(derived(spec));
out
}
fn declared(d: &Declared, default: &Position) -> Option<Resolved> {
let position = d.from.as_ref().map_or_else(|| default.clone(), |f| Position::Machine(f.clone()));
let (probe, default_wait, what) = if let Some(u) = &d.http {
declared_http(d, u)?
} else if let Some(t) = &d.tcp {
declared_tcp(d, t)?
} else if let Some(c) = &d.exec {
let expect = match &d.expect {
None => None,
Some(Expect::Text(t)) => Some(t.clone()),
Some(Expect::Status(_)) => return None,
};
(Probe::Exec { command: c.clone(), expect }, 0, c.clone())
} else {
let p = d.script.as_ref()?;
(Probe::Script { path: p.clone() }, 0, p.clone())
};
Some(Resolved {
name: d.name.clone().unwrap_or_else(|| match &position {
Position::Machine(m) => format!("{what} from {m}"),
Position::Networks => what,
}),
position,
probe,
wait: d.wait.unwrap_or(default_wait),
derived: false,
})
}
type DeclaredProbe = (Probe, u32, String);
fn declared_http(d: &Declared, u: &str) -> Option<DeclaredProbe> {
let url = Url::parse(u)?;
let expect = match &d.expect {
None => HttpExpect::Status(200),
Some(Expect::Status(s)) => HttpExpect::Status(*s),
Some(Expect::Text(t)) if t == "any" => HttpExpect::Any,
Some(Expect::Text(t)) if t == "blocked" => HttpExpect::Blocked,
Some(Expect::Text(_)) => return None,
};
let wait = if expect == HttpExpect::Blocked { 0 } else { 30 };
let request = HttpRequest {
method: d.method.clone(),
headers: d.headers.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
body: d.body.clone(),
contains: d.contains.clone(),
follow: d.follow,
};
let what = match (&expect, &request.method) {
(HttpExpect::Blocked, _) => format!("{u} blocked"),
(_, Some(m)) => format!("{} {u}", m.to_uppercase()),
_ => u.to_owned(),
};
Some((Probe::Http { url, expect, request }, wait, what))
}
fn declared_tcp(d: &Declared, target: &str) -> Option<DeclaredProbe> {
let (host, port) = target.rsplit_once(':')?;
let port: u16 = port.parse().ok().filter(|p| *p > 0)?;
let expect = match d.expect.as_ref() {
None => TcpExpect::Open,
Some(Expect::Text(t)) if t == "open" => TcpExpect::Open,
Some(Expect::Text(t)) if t == "blocked" => TcpExpect::Blocked,
_ => return None,
};
let wait = if expect == TcpExpect::Blocked { 0 } else { 30 };
let what = match expect {
TcpExpect::Blocked => format!("{target} blocked"),
TcpExpect::Open => target.to_owned(),
};
let exchange = TcpExchange {
send: d.send.clone(),
contains: d.contains.clone(),
};
let probe = Probe::Tcp {
host: Host::Literal(host.to_owned()),
port,
expect,
exchange,
};
Some((probe, wait, what))
}
#[must_use]
pub fn by_position<'a>(spec: &Spec, checks: &'a [Resolved]) -> Vec<(Position, Vec<&'a Resolved>)> {
let mut order: Vec<Position> = vec![default_position(spec)];
for m in spec.machines.keys() {
let p = Position::Machine(m.clone());
if !order.contains(&p) {
order.push(p);
}
}
if !order.contains(&Position::Networks) {
order.push(Position::Networks);
}
order
.into_iter()
.filter_map(|p| {
let group: Vec<&Resolved> = checks.iter().filter(|c| c.position == p).collect();
(!group.is_empty()).then_some((p, group))
})
.collect()
}
#[cfg(test)]
mod tests;