use cyberbrain_core::{Error, Result};
use cyberbrain_policy::bundle;
use std::path::PathBuf;
pub mod access;
pub mod admin;
pub mod api;
pub mod client;
pub mod licence;
pub mod page;
pub mod report;
pub mod service;
pub mod store;
pub mod tls;
#[cfg(test)]
mod tests;
#[cfg_attr(not(test), allow(unused_imports))]
pub use store::{Device, HubStore};
pub const DEFAULT_DATA: &str = "cyberbrain-hub.db";
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Accepted {
pub device: String,
pub accepted: usize,
pub next_anchor: String,
pub total_rows: i64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LicenceState {
Missing,
Invalid(String),
Valid {
customer: String,
seats: usize,
valid_until: String,
warning: Option<String>,
},
Expired {
customer: String,
valid_until: String,
},
}
impl LicenceState {
pub fn read(hub: &HubStore, now: jiff::Timestamp) -> Self {
let Ok(Some(text)) = hub.licence_text() else {
return LicenceState::Missing;
};
match licence::parse(&text) {
Err(e) => LicenceState::Invalid(e.to_string()),
Ok(l) if l.not_yet_valid(now) => LicenceState::Invalid(format!(
"the licence for {} does not start until {}",
l.licence().customer,
l.licence().valid_from
)),
Ok(l) if l.expired(now) => LicenceState::Expired {
customer: l.licence().customer.clone(),
valid_until: l.licence().valid_until.clone(),
},
Ok(l) => LicenceState::Valid {
customer: l.licence().customer.clone(),
seats: l.licence().seats,
valid_until: l.licence().valid_until.clone(),
warning: l.warning(now),
},
}
}
pub fn may_collect(&self) -> bool {
matches!(self, LicenceState::Valid { .. })
}
pub fn seats(&self) -> Option<usize> {
match self {
LicenceState::Valid { seats, .. } => Some(*seats),
_ => None,
}
}
pub fn line(&self) -> String {
match self {
LicenceState::Missing => concat!(
"no licence installed: the hub will not accept rows. ",
"Install one with `cyberbrain hub licence install <file>`."
)
.to_string(),
LicenceState::Invalid(why) => format!("licence not usable: {why}"),
LicenceState::Expired {
customer,
valid_until,
} => format!(
concat!(
"licence for {} ended on {}. New rows are not accepted; the record ",
"stays readable and exportable, and clients keep buffering."
),
customer, valid_until
),
LicenceState::Valid {
customer,
seats,
valid_until,
warning,
} => match warning {
Some(w) => format!("⚠ {w}"),
None => format!("licence: {customer}, {seats} seat(s), until {valid_until}"),
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Refusal {
NotAuthorised(String),
BadBundle(String),
WrongAnchor { expected: String, got: String },
NotCollecting(String),
}
impl std::fmt::Display for Refusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Refusal::NotAuthorised(m) => write!(f, "{m}"),
Refusal::BadBundle(m) => write!(f, "{m}"),
Refusal::WrongAnchor { expected, got } => write!(
f,
"this device's chain is at {expected}, the delivery starts at {got}: \
something is missing between them, or this was already delivered"
),
Refusal::NotCollecting(m) => write!(f, "{m}"),
}
}
}
pub fn ingest(
hub: &mut HubStore,
licence: &LicenceState,
token: Option<&str>,
body: &str,
version: Option<&str>,
now: &str,
) -> std::result::Result<Accepted, Refusal> {
if !licence.may_collect() {
return Err(Refusal::NotCollecting(format!(
"{} Keep buffering: nothing is lost, and a renewed licence takes what you held.",
licence.line()
)));
}
let token = token.ok_or_else(|| {
Refusal::NotAuthorised("no device token; send it as `Authorization: Bearer …`".into())
})?;
let device = hub
.device_by_token(token)
.map_err(|e| Refusal::NotAuthorised(format!("cannot check the token: {e}")))?
.ok_or_else(|| Refusal::NotAuthorised("unknown device token".into()))?;
if !device.is_active() {
return Err(Refusal::NotAuthorised(format!(
"device {} was revoked; its record is kept, but it may not send",
device.id
)));
}
let note = |r: Refusal| -> Refusal {
let _ = hub.note_refusal(&device.id, &r.to_string(), now);
r
};
let (report, rows) =
bundle::verify_rows(body).map_err(|e| note(Refusal::BadBundle(e.to_string())))?;
let fresh: &[cyberbrain_policy::AuditEvent] = if report.anchor == device.anchor {
&rows
} else if let Some(i) = rows
.iter()
.position(|r| r.chain_hash() == Some(device.anchor.as_str()))
{
&rows[i + 1..]
} else {
return Err(note(Refusal::WrongAnchor {
expected: device.anchor.clone(),
got: report.anchor,
}));
};
let new_anchor = fresh
.last()
.and_then(|r| r.chain_hash())
.unwrap_or(&device.anchor)
.to_string();
let total = hub
.append(&device, fresh, &new_anchor, version, now)
.map_err(|e| Refusal::BadBundle(format!("could not store the delivery: {e}")))?;
Ok(Accepted {
device: device.id,
accepted: fresh.len(),
next_anchor: new_anchor,
total_rows: total,
})
}
const PIN_SETTING: &str = "tls_cert_sha256";
pub fn remember_pin(hub: &HubStore, pin: Option<&str>) -> Result<()> {
match pin {
Some(p) => hub.set_setting(PIN_SETTING, p),
None => hub.clear_setting(PIN_SETTING),
}
}
pub fn pin_to_offer(hub: &HubStore) -> Option<String> {
hub.setting(PIN_SETTING)
.ok()
.flatten()
.filter(|p| !p.is_empty())
}
pub fn data_path(explicit: Option<PathBuf>) -> PathBuf {
explicit.unwrap_or_else(|| PathBuf::from(DEFAULT_DATA))
}
pub fn parse_addr(s: &str) -> Result<std::net::SocketAddr> {
s.parse()
.map_err(|e| Error::Config(format!("--addr {s:?} is not an address:port pair: {e}")))
}