1pub mod date;
6
7use std::process::{Command, Stdio};
8
9use url::{Host, Url};
10
11pub const ALLOW_INSECURE_URL_ENV: &str = "SANDOGASA_ALLOW_INSECURE_URL";
16
17pub fn ensure_secure_url(base_url: &str) -> Result<(), String> {
30 let allow_insecure = std::env::var_os(ALLOW_INSECURE_URL_ENV).is_some_and(|v| !v.is_empty());
31 check_secure_url(base_url, allow_insecure)
32}
33
34fn check_secure_url(base_url: &str, allow_insecure: bool) -> Result<(), String> {
37 let parsed = Url::parse(base_url).map_err(|e| format!("invalid URL '{base_url}': {e}"))?;
38 if parsed.scheme() == "https" || host_is_loopback(&parsed) {
39 return Ok(());
40 }
41 if allow_insecure {
42 return Ok(());
43 }
44 Err(format!(
45 "refusing to send credentials to '{base_url}' over plaintext \
46 {}: use an https URL, or set {ALLOW_INSECURE_URL_ENV}=1 to \
47 override (e.g. for local testing against a mock server).",
48 parsed.scheme()
49 ))
50}
51
52fn host_is_loopback(u: &Url) -> bool {
54 match u.host() {
55 Some(Host::Domain(d)) => d == "localhost" || d.ends_with(".localhost"),
56 Some(Host::Ipv4(ip)) => ip.is_loopback(),
57 Some(Host::Ipv6(ip)) => ip.is_loopback(),
58 None => false,
59 }
60}
61
62pub fn require_tool_with_arg(
76 name: &str,
77 version_arg: &str,
78 install_hint: &str,
79) -> Result<(), String> {
80 match Command::new(name)
81 .arg(version_arg)
82 .stdout(Stdio::null())
83 .stderr(Stdio::null())
84 .status()
85 {
86 Ok(s) if s.success() => Ok(()),
87 Ok(s) => Err(format!(
88 "{name} exited with {s}; is it installed correctly? \
89 Install it with: {install_hint}"
90 )),
91 Err(_) => Err(format!("{name} not found. Install it with: {install_hint}")),
92 }
93}
94
95pub fn require_tool(name: &str, install_hint: &str) -> Result<(), String> {
109 require_tool_with_arg(name, "--version", install_hint)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn require_missing_tool() {
118 let result = require_tool("nonexistent_tool_xyz_123", "magic install");
119 assert!(result.is_err());
120 let msg = result.unwrap_err();
121 assert!(msg.contains("nonexistent_tool_xyz_123"));
122 assert!(msg.contains("magic install"));
123 }
124
125 #[test]
126 fn require_available_tool() {
127 let result = require_tool("sh", "should already be installed");
131 let _ = result;
134 }
135
136 #[test]
137 fn secure_url_allows_https() {
138 assert!(check_secure_url("https://bugzilla.redhat.com", false).is_ok());
139 assert!(check_secure_url("https://gitlab.com/api/v4", false).is_ok());
140 }
141
142 #[test]
143 fn secure_url_allows_loopback_over_http() {
144 assert!(check_secure_url("http://127.0.0.1:8080", false).is_ok());
146 assert!(check_secure_url("http://localhost:3000/api", false).is_ok());
147 assert!(check_secure_url("http://[::1]:9999", false).is_ok());
148 }
149
150 #[test]
151 fn secure_url_rejects_plaintext_remote() {
152 let err = check_secure_url("http://gitlab.example.com", false).unwrap_err();
153 assert!(err.contains("gitlab.example.com"));
154 assert!(err.contains(ALLOW_INSECURE_URL_ENV));
155 }
156
157 #[test]
158 fn secure_url_override_allows_plaintext_remote() {
159 assert!(check_secure_url("http://gitlab.example.com", true).is_ok());
161 }
162
163 #[test]
164 fn secure_url_rejects_invalid() {
165 assert!(check_secure_url("not a url", false).is_err());
166 }
167}