use std::sync::Arc;
use crate::*;
const CREATED_AT_CLOCK_SKEW_ALLOWED_MICROS: i64 =
std::time::Duration::from_secs(60 * 3).as_micros() as i64;
const EXPIRES_AT_DURATION_MAX_ALLOWED_MICROS: i64 =
std::time::Duration::from_secs(60 * 30).as_micros() as i64;
struct ThreadGuard(&'static str);
impl Drop for ThreadGuard {
fn drop(&mut self) {
tracing::debug!("{}", self.0);
}
}
pub struct BootstrapSrv {
cont: Arc<std::sync::atomic::AtomicBool>,
workers: Vec<std::thread::JoinHandle<std::io::Result<()>>>,
addrs: Vec<std::net::SocketAddr>,
server: Option<Server>,
}
impl Drop for BootstrapSrv {
fn drop(&mut self) {
let _g = ThreadGuard("Server Shutdown Complete!");
tracing::debug!("begin server shutdown...");
let _ = self.shutdown();
}
}
impl BootstrapSrv {
pub fn new(config: Config) -> std::io::Result<Self> {
let config = Arc::new(config);
let cont = Arc::new(std::sync::atomic::AtomicBool::new(true));
let space_map = crate::SpaceMap::default();
let sconf = ServerConfig {
addrs: config.listen_address_list.clone(),
worker_thread_count: config.worker_thread_count,
tls_config: if let (Some(cert), Some(key)) =
(&config.tls_cert, &config.tls_key)
{
Some(TlsConfig::new(cert.clone(), key.clone()))
} else {
None
},
};
let store = Arc::new(crate::Store::default());
let server = Server::new(config.clone(), sconf)
.map_err(std::io::Error::other)?;
let addrs = server.server_addrs().to_vec();
tracing::info!(?addrs, "Listening");
for addr in addrs.iter() {
println!("#kitsune2_bootstrap_srv#listening#{addr:?}#");
}
println!("#kitsune2_bootstrap_srv#running#");
let mut workers = Vec::with_capacity(config.worker_thread_count + 1);
for _ in 0..config.worker_thread_count {
let config = config.clone();
let cont = cont.clone();
let store = store.clone();
let recv = server.receiver().clone();
let space_map = space_map.clone();
workers.push(std::thread::spawn(move || {
worker(config, cont, store, recv, space_map)
}));
}
let prune_cont = cont.clone();
let prune_space_map = space_map.clone();
workers.push(std::thread::spawn(move || {
prune_worker(config, prune_cont, prune_space_map)
}));
Ok(Self {
cont,
workers,
addrs,
server: Some(server),
})
}
pub fn shutdown(&mut self) -> std::io::Result<()> {
let mut is_err = false;
self.cont.store(false, std::sync::atomic::Ordering::SeqCst);
drop(self.server.take());
while !self.workers.is_empty() {
tracing::debug!(
"waiting on {} threads to close...",
self.workers.len()
);
if self.workers.pop().unwrap().join().is_err() {
is_err = true;
}
}
tracing::debug!("all threads closed.");
if is_err {
Err(std::io::Error::other("Failure shutting down worker thread"))
} else {
Ok(())
}
}
pub fn listen_addrs(&self) -> &[std::net::SocketAddr] {
self.addrs.as_slice()
}
}
fn prune_worker(
config: Arc<Config>,
cont: Arc<std::sync::atomic::AtomicBool>,
space_map: crate::SpaceMap,
) -> std::io::Result<()> {
let _g = ThreadGuard("prune_worker thread has ended");
let mut last_check = std::time::Instant::now();
while cont.load(std::sync::atomic::Ordering::SeqCst) {
std::thread::sleep(config.request_listen_duration);
if last_check.elapsed() >= config.prune_interval {
last_check = std::time::Instant::now();
space_map.update_all(config.max_entries_per_space);
}
}
Ok(())
}
fn worker(
config: Arc<Config>,
cont: Arc<std::sync::atomic::AtomicBool>,
store: Arc<crate::Store>,
recv: HttpReceiver,
space_map: crate::SpaceMap,
) -> std::io::Result<()> {
let _g = ThreadGuard("worker thread has ended");
while cont.load(std::sync::atomic::Ordering::SeqCst) {
let (req, res) = match recv.recv() {
None => break,
Some(r) => r,
};
let handler = Handler {
config: &config,
store: &store,
space_map: &space_map,
res,
};
handler.handle(req)?;
}
Ok(())
}
struct Handler<'lt> {
config: &'lt Config,
store: &'lt crate::Store,
space_map: &'lt crate::SpaceMap,
res: HttpRespondCb,
}
impl Handler<'_> {
pub fn handle(mut self, req: HttpRequest) -> std::io::Result<()> {
match self.handle_inner(req) {
Ok((status, body)) => self.respond(status, body),
Err(err) => self.respond(
500,
serde_json::to_string(&serde_json::json!({
"error": format!("{err:?}"),
}))?
.into_bytes(),
),
}
Ok(())
}
fn handle_inner(
&mut self,
req: HttpRequest,
) -> std::io::Result<(u16, Vec<u8>)> {
match req {
HttpRequest::HealthGet => Ok((200, b"{}".to_vec())),
HttpRequest::BootstrapGet { space } => self.handle_boot_get(space),
HttpRequest::BootstrapPut { space, agent, body } => {
self.handle_boot_put(space, agent, body)
}
}
}
fn handle_boot_get(
&mut self,
space: bytes::Bytes,
) -> std::io::Result<(u16, Vec<u8>)> {
let res = self.space_map.read(&space)?;
Ok((200, res))
}
fn handle_boot_put(
&mut self,
space: bytes::Bytes,
agent: bytes::Bytes,
body: bytes::Bytes,
) -> std::io::Result<(u16, Vec<u8>)> {
use ed25519_dalek::*;
let now = crate::now();
let info = crate::ParsedEntry::try_from_slice(&body)?;
if *agent != *info.agent.as_bytes() {
return Err(std::io::Error::other("InvalidAgent"));
}
if space != info.space {
return Err(std::io::Error::other("InvalidSpace"));
}
if info.created_at < now - CREATED_AT_CLOCK_SKEW_ALLOWED_MICROS {
return Err(std::io::Error::other("InvalidCreatedAt"));
}
if info.created_at > now + CREATED_AT_CLOCK_SKEW_ALLOWED_MICROS {
return Err(std::io::Error::other("InvalidCreatedAt"));
}
if info.expires_at < now {
return Err(std::io::Error::other("InvalidExpiresAt"));
}
if info.expires_at <= info.created_at {
return Err(std::io::Error::other("InvalidExpiresAt"));
}
if info.expires_at - info.created_at
> EXPIRES_AT_DURATION_MAX_ALLOWED_MICROS
{
return Err(std::io::Error::other("InvalidExpiresAt"));
}
tracing::info!("Basic checks passed");
info.agent
.verify(info.encoded.as_bytes(), &info.signature)
.map_err(|err| {
std::io::Error::other(format!("InvalidSignature: {err:?}"))
})?;
let r = if info.is_tombstone {
None
} else {
Some(self.store.write(&body)?)
};
self.space_map.update(
self.config.max_entries_per_space,
space,
Some((info, r)),
);
Ok((200, b"{}".to_vec()))
}
fn respond(self, status: u16, body: Vec<u8>) {
let Self { res, .. } = self;
res(HttpResponse { status, body });
}
}