use crate::config::{Config, Credential};
use std::sync::Mutex;
use zeroize::Zeroizing;
pub(super) struct Revoke {
held: Mutex<Option<Request>>,
}
struct Request {
issuer: String,
agent: String,
pass: Zeroizing<String>,
response_bytes: usize,
timeout_ms: u64,
}
impl Revoke {
pub fn new(config: &Config) -> Result<Option<Self>, String> {
let Credential::Lys(lys) = &config.credential else {
return Ok(None);
};
if !lys.key_file.as_os_str().is_empty() {
return Ok(None);
}
let agent = config
.agent
.as_deref()
.ok_or("the run has no agent to revoke")?;
let environment = std::env::var("LYS_AGENT")
.map_err(|_| "LYS_AGENT is unavailable at the revoke boundary")?;
if environment != agent {
return Err("the revoke agent differs from LYS_AGENT".to_owned());
}
if !agent
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
return Err("revoke agent must be one plain path segment".to_owned());
}
let issuer = origin(&lys.issuer)?;
let pass = Zeroizing::new(
std::env::var("LYS_AGENT_PASS")
.map_err(|_| "LYS_AGENT_PASS is unavailable at the revoke boundary")?,
);
if pass.is_empty()
|| pass.len() > config.listen.run_pass_bytes
|| !pass.is_ascii()
|| pass.bytes().any(|byte| byte.is_ascii_control())
{
return Err(format!(
"LYS_AGENT_PASS must be a nonempty header of at most {} bytes",
config.listen.run_pass_bytes
));
}
Ok(Some(Self {
held: Mutex::new(Some(Request {
issuer: issuer.to_owned(),
agent: agent.to_owned(),
pass,
response_bytes: config.listen.issuer_response_bytes,
timeout_ms: config.limits.reply_timeout_ms,
})),
}))
}
pub fn send(&self) -> Result<(), String> {
let held = self
.held
.lock()
.map_err(|_| "the revoke lock is poisoned")?
.take();
held.map_or(Ok(()), |held| {
crate::agent::exit::revoke_run_pass(
&held.issuer,
&held.agent,
held.pass.to_string(),
held.response_bytes,
held.timeout_ms,
)
.map_err(|error| error.to_string())
})
}
}
fn origin(issuer: &str) -> Result<&str, String> {
let issuer = issuer.trim_end_matches('/');
let uri: ureq::http::Uri = issuer.parse().map_err(|_| "lys.issuer is not an origin")?;
let authority = uri.authority().ok_or("lys.issuer has no authority")?;
if authority.as_str().contains('@')
|| uri.query().is_some()
|| !matches!(uri.path(), "" | "/")
|| issuer.contains('#')
{
return Err(
"lys.issuer must be an origin without credentials, path, query or fragment".to_owned(),
);
}
if uri.scheme_str() != Some("https")
&& !(uri.scheme_str() == Some("http")
&& ["localhost", "127.0.0.1", "[::1]"].contains(&authority.host()))
{
return Err("lys.issuer must be https or loopback http".to_owned());
}
Ok(issuer)
}