pub mod date;
use std::process::{Command, Stdio};
use url::{Host, Url};
pub fn init() {
install_crypto_provider();
}
pub fn install_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
pub const ALLOW_INSECURE_URL_ENV: &str = "SANDOGASA_ALLOW_INSECURE_URL";
pub fn ensure_secure_url(base_url: &str) -> Result<(), String> {
let allow_insecure = std::env::var_os(ALLOW_INSECURE_URL_ENV).is_some_and(|v| !v.is_empty());
check_secure_url(base_url, allow_insecure)
}
fn check_secure_url(base_url: &str, allow_insecure: bool) -> Result<(), String> {
let parsed = Url::parse(base_url).map_err(|e| format!("invalid URL '{base_url}': {e}"))?;
if parsed.scheme() == "https" || host_is_loopback(&parsed) {
return Ok(());
}
if allow_insecure {
return Ok(());
}
Err(format!(
"refusing to send credentials to '{base_url}' over plaintext \
{}: use an https URL, or set {ALLOW_INSECURE_URL_ENV}=1 to \
override (e.g. for local testing against a mock server).",
parsed.scheme()
))
}
fn host_is_loopback(u: &Url) -> bool {
match u.host() {
Some(Host::Domain(d)) => d == "localhost" || d.ends_with(".localhost"),
Some(Host::Ipv4(ip)) => ip.is_loopback(),
Some(Host::Ipv6(ip)) => ip.is_loopback(),
None => false,
}
}
pub fn tool_exists(name: &str) -> bool {
std::env::var_os("PATH")
.map(|paths| std::env::split_paths(&paths).any(|dir| dir.join(name).is_file()))
.unwrap_or(false)
}
fn tool_available(exe: &str, probe: Option<&str>) -> bool {
match probe {
Some(arg) => Command::new(exe)
.arg(arg)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success()),
None => tool_exists(exe),
}
}
pub fn require_tools(tools: &[(&str, &str, Option<&str>)]) -> Result<(), String> {
let missing: Vec<String> = tools
.iter()
.filter(|(exe, _, probe)| !tool_available(exe, *probe))
.map(|(exe, hint, _)| format!("{exe} (install: {hint})"))
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(format!("missing required tool(s): {}", missing.join(", ")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_exists_detects_present_and_absent() {
assert!(tool_exists("sh"));
assert!(!tool_exists("nonexistent_tool_xyz_123"));
}
#[test]
fn require_tools_path_and_probe_modes() {
assert!(require_tools(&[("sh", "present", None)]).is_ok());
assert!(require_tools(&[("nonexistent_zzz", "install zzz", None)]).is_err());
assert!(require_tools(&[("true", "ok", Some("--version"))]).is_ok());
let err = require_tools(&[
("true", "ok", Some("--version")),
("nonexistent_aaa_111", "install aaa", Some("--version")),
("nonexistent_bbb_222", "install bbb", None),
])
.unwrap_err();
assert!(err.contains("nonexistent_aaa_111"));
assert!(err.contains("install aaa"));
assert!(err.contains("nonexistent_bbb_222"));
assert!(err.contains("install bbb"));
assert!(!err.contains("true ("));
}
#[test]
fn secure_url_allows_https() {
assert!(check_secure_url("https://bugzilla.redhat.com", false).is_ok());
assert!(check_secure_url("https://gitlab.com/api/v4", false).is_ok());
}
#[test]
fn secure_url_allows_loopback_over_http() {
assert!(check_secure_url("http://127.0.0.1:8080", false).is_ok());
assert!(check_secure_url("http://localhost:3000/api", false).is_ok());
assert!(check_secure_url("http://[::1]:9999", false).is_ok());
}
#[test]
fn secure_url_rejects_plaintext_remote() {
let err = check_secure_url("http://gitlab.example.com", false).unwrap_err();
assert!(err.contains("gitlab.example.com"));
assert!(err.contains(ALLOW_INSECURE_URL_ENV));
}
#[test]
fn secure_url_override_allows_plaintext_remote() {
assert!(check_secure_url("http://gitlab.example.com", true).is_ok());
}
#[test]
fn secure_url_rejects_invalid() {
assert!(check_secure_url("not a url", false).is_err());
}
}