use std::net::{IpAddr, TcpStream};
use std::time::{Duration, Instant};
use url::Url;
use super::lys_pass::LysAgent;
pub(super) struct Options {
pub url: String,
pub agent: LysAgent,
pub timeout: Duration,
}
impl Options {
pub fn parse(mut args: impl Iterator<Item = String>) -> Result<Self, String> {
let mut remote = None;
let mut pass_args = Vec::new();
while let Some(arg) = args.next() {
if arg == "--remote" {
if remote.is_some() {
return Err("haematite_mcp_remote_arguments_invalid: repeated --remote".into());
}
remote = args.next();
if remote.is_none() {
return Err(
"haematite_mcp_remote_arguments_invalid: --remote requires a URL".into(),
);
}
} else {
pass_args.push(arg);
}
}
let remote = remote.ok_or("haematite_mcp_remote_arguments_invalid: missing --remote")?;
let url = validate(&remote, "haematite_mcp_remote_url_invalid")?;
let issuer = pass_args
.windows(2)
.find(|pair| pair[0] == "--lys-agent")
.map(|pair| pair[1].as_str())
.ok_or(
"haematite_mcp_remote_arguments_invalid: --lys-agent requires an issuer and agent",
)?;
let lys = validate(issuer, "haematite_mcp_lys_url_invalid")?;
let timeout_ms = pass_args
.windows(2)
.find(|pair| pair[0] == "--timeout-ms")
.map_or(Ok(10_000), |pair| pair[1].parse::<u64>())
.map_err(|_| "haematite_mcp_remote_arguments_invalid: invalid timeout")?;
let timeout = Duration::from_millis(timeout_ms);
let agent = super::pass_header::parse(pass_args.clone().into_iter())?;
probe(&url, &remote, timeout, "haematite_mcp_remote_unanswered")?;
probe(&lys, issuer, timeout, "haematite_mcp_lys_unanswered")?;
Ok(Self {
url: remote,
agent,
timeout,
})
}
}
fn validate(value: &str, code: &str) -> Result<Url, String> {
let invalid = || {
format!(
"{code}: requires HTTPS, or HTTP on 127.0.0.1 or localhost; no credentials or fragment"
)
};
let uri: ureq::http::Uri = value.parse().map_err(|_| invalid())?;
let authority = uri.authority().ok_or_else(invalid)?;
let local = matches!(authority.host(), "127.0.0.1" | "localhost");
if !value.is_ascii()
|| value.contains('#')
|| authority.as_str().contains('@')
|| !(uri.scheme_str() == Some("https") || (uri.scheme_str() == Some("http") && local))
{
return Err(invalid());
}
Url::parse(value).map_err(|_| invalid())
}
fn probe(url: &Url, address: &str, timeout: Duration, code: &str) -> Result<(), String> {
let host = url.host_str().unwrap_or_default().trim_matches(['[', ']']);
if host != "localhost" && !host.parse::<IpAddr>().is_ok_and(|ip| ip.is_loopback()) {
return Ok(());
}
let unanswered = || format!("{code}: {address} did not answer");
let started = Instant::now();
let addresses = url.socket_addrs(|| None).map_err(|_| unanswered())?;
for socket in addresses {
let remaining = timeout
.checked_sub(started.elapsed())
.ok_or_else(unanswered)?;
if TcpStream::connect_timeout(&socket, remaining).is_ok() {
return Ok(());
}
}
Err(unanswered())
}