use std::path::Path;
pub use plugin_door_wire::attach::{EnrolReply, EnrolRequest, InstalledNotice, ENROL_PATH, INSTALLED_PATH};
use crate::config::FleetConfig;
pub fn post(door: &str, req: &EnrolRequest) -> Result<EnrolReply, String> {
let body = post_json(door, ENROL_PATH, req)?;
serde_json::from_value(body).map_err(|e| format!("{door}{ENROL_PATH}: not an enrolment reply: {e}"))
}
pub fn notice(estate: &str, fleet_keys: &std::collections::BTreeMap<String, String>, ticket_key: &str, nonce: &str, at_unix_ms: i64) -> InstalledNotice {
InstalledNotice {
v: 1,
estate: estate.trim().to_string(),
product: crate::PRODUCT.into(),
roles: fleet_keys.keys().cloned().collect(),
nonce: nonce.to_string(),
at_unix_ms,
ticket_key: ticket_key.trim().to_ascii_lowercase(),
fleet_keys: fleet_keys.iter().map(|(r, k)| (r.clone(), k.trim().to_ascii_lowercase())).collect(),
owner_sig: String::new(),
}
}
pub fn installed(door: &str, n: &InstalledNotice) -> Result<serde_json::Value, String> {
post_json(door, INSTALLED_PATH, n)
}
fn post_json<T: serde::Serialize>(door: &str, path: &str, body: &T) -> Result<serde_json::Value, String> {
let url = format!("{}{path}", door.trim_end_matches('/'));
let agent: ureq::Agent = ureq::Agent::config_builder().http_status_as_error(false).build().into();
let mut resp = agent.post(&url).send_json(body).map_err(|e| format!("{url}: {e}"))?;
let status = resp.status();
let v: serde_json::Value = resp.body_mut().read_json().map_err(|e| format!("{url} answered {status} with no JSON: {e}"))?;
if !status.is_success() {
let clause = v.get("refused").and_then(|x| x.as_str()).unwrap_or("refused");
let why = v.get("why").and_then(|x| x.as_str()).unwrap_or("");
return Err(format!("REFUSED [{clause}] {why}"));
}
Ok(v)
}
pub fn remember(root: &Path, door: &str, req: &EnrolRequest, reply: &EnrolReply) -> Result<FleetConfig, String> {
if crate::key32(&reply.monetize_pubkey).is_none() {
return Err(format!("the central answered a monetize key that is not 32 bytes ({:?})", reply.monetize_pubkey));
}
let cfg = FleetConfig { url: door.trim_end_matches('/').to_string(), instance: req.estate.clone(), monetize_pubkey: reply.monetize_pubkey.clone(), ..Default::default() };
cfg.save(root).map_err(|e| format!("keeping the fleet config: {e}"))?;
Ok(cfg)
}
#[cfg(test)]
mod tests {
use super::*;
fn req() -> EnrolRequest {
EnrolRequest {
v: 1,
token: "att1.a.b".into(),
estate: "acme".into(),
product: "holger".into(),
box_name: "main".into(),
csr_pem: String::new(),
endpoint: "10.0.0.2:50051".into(),
owner_key: String::new(),
estate_ca_pem: String::new(),
nonce: "n".into(),
at_unix_ms: 1,
owner_sig: String::new(),
}
}
fn reply(key: &str) -> EnrolReply {
EnrolReply { cert_pem: "CERT".into(), attach_ca_pem: String::new(), monetize_identity: String::new(), monetize_identity_key: String::new(), monetize_pubkey: key.into(), plugin_door: String::new(), product: "holger".into(), estate_bearer: "b".into() }
}
#[test]
fn the_installed_notice_names_each_box_with_its_key_and_the_ticket_key() {
let keys = std::collections::BTreeMap::from([("main".to_string(), "aa".repeat(32)), ("twin".to_string(), "bb".repeat(32))]);
let n = notice("acme", &keys, &"aa".repeat(32), "n-1", 7);
assert_eq!((n.v, n.estate.as_str(), n.product.as_str(), n.roles.clone()), (1, "acme", "holger", vec!["main".to_string(), "twin".to_string()]));
assert_eq!(n.ticket_key, "aa".repeat(32));
let signed = String::from_utf8(n.signed_bytes()).unwrap();
assert!(signed.starts_with("monetize-attach-installed v1") && signed.contains(&format!("fleet \"twin\" \"{}\"", "bb".repeat(32))), "{signed}");
assert!(n.owner_sig.is_empty(), "the owner signs it, not this function");
}
#[test]
fn an_enrolment_keeps_the_door_the_estate_and_the_key_and_refuses_a_bad_key() {
let dir = tempfile::tempdir().unwrap();
let cfg = remember(dir.path(), "https://monetize.ignalina.dk/", &req(), &reply(&"ab".repeat(32))).unwrap();
assert_eq!((cfg.url.as_str(), cfg.instance.as_str(), cfg.key()), ("https://monetize.ignalina.dk", "acme", Some([0xab; 32])));
assert_eq!(FleetConfig::load(dir.path()).unwrap(), Some(cfg), "the pull channel reads it on its next start");
assert!(!dir.path().join("fleet/attach-cert.pem").exists(), "no certificate is kept: the channel presents none");
assert!(remember(dir.path(), "https://x", &req(), &reply("nope")).unwrap_err().contains("not 32 bytes"));
}
}