use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::Deserialize;
use std::{collections::BTreeMap, io::Read as _, path::Path};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Proof {
headers: BTreeMap<String, String>,
}
pub(in crate::assistant) fn read(path: &str, bound: usize) -> anyhow::Result<String> {
use std::os::unix::fs::{MetadataExt as _, OpenOptionsExt as _, PermissionsExt as _};
anyhow::ensure!(
Path::new(path).is_absolute(),
"assistant_needs_lys: proof_file_must_be_absolute"
);
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(i32::try_from(
(rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::NONBLOCK).bits(),
)?)
.open(path)
.map_err(|error| {
anyhow::anyhow!(
"assistant_needs_lys: proof_file_unreadable: {}",
error.kind()
)
})?;
let metadata = file.metadata().map_err(|error| {
anyhow::anyhow!(
"assistant_needs_lys: proof_file_metadata_failed: {}",
error.kind()
)
})?;
anyhow::ensure!(
metadata.is_file()
&& metadata.uid() == rustix::process::geteuid().as_raw()
&& metadata.permissions().mode().trailing_zeros() >= 6,
"assistant_needs_lys: proof_file_not_private"
);
let mut bytes = Vec::new();
file.take(u64::try_from(bound)?.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(|error| {
anyhow::anyhow!(
"assistant_needs_lys: proof_file_unreadable: {}",
error.kind()
)
})?;
anyhow::ensure!(
bytes.len() <= bound,
"assistant_needs_lys: proof_file_exceeds_bound"
);
let text = std::str::from_utf8(&bytes)
.map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_file_invalid"))?;
let validated: Proof = serde_json::from_str(text)
.map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_file_invalid"))?;
let canonical = serde_json::to_string(&serde_json::json!({"headers":validated.headers}))?;
headers(&canonical)?;
Ok(canonical)
}
pub(in crate::assistant) fn headers(text: &str) -> anyhow::Result<HeaderMap> {
let proof: Proof = serde_json::from_str(text)
.map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_headers_invalid"))?;
anyhow::ensure!(
proof.headers.contains_key("lys-agent-pass") && proof.headers.len() <= 2,
"assistant_needs_lys: proof_headers_invalid"
);
let mut headers = HeaderMap::new();
for (name, value) in proof.headers {
anyhow::ensure!(
matches!(name.as_str(), "lys-agent-pass" | "lys-seat")
&& !value.is_empty()
&& value.bytes().all(|byte| byte.is_ascii_graphic()),
"assistant_needs_lys: proof_headers_invalid"
);
let name = HeaderName::from_bytes(name.as_bytes())
.map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_header_name_invalid"))?;
let mut value = HeaderValue::from_str(&value)
.map_err(|_| anyhow::anyhow!("assistant_needs_lys: proof_header_value_invalid"))?;
value.set_sensitive(true);
headers.insert(name, value);
}
Ok(headers)
}