1use std::fmt;
4use std::future::Future;
5use std::io;
6use std::sync::Arc;
7use std::time::Duration;
8
9use tokio::net::TcpListener;
10
11use crate::api::{self, App, Limits, TlsError};
12use crate::clock::{Clock, SystemClock};
13use crate::config::Config;
14use crate::datadir::{self, LockError, Work};
15use crate::envelope::{Key, KeyError};
16use crate::fs::OsFs;
17use crate::retention::Retention;
18use crate::tenants::{KeyOrder, RotationBacklog, TenantError, Tenants};
19use time::OffsetDateTime;
20
21#[derive(Debug)]
23pub enum ServeError {
24 DataDir(LockError),
26 MasterKey(KeyError),
28 WrongMasterKey,
30 Registry(TenantError),
32 KeysOutOfOrder(KeyOrder),
34 Tls(TlsError),
36 Listen(io::Error),
38 Serve(io::Error),
40}
41
42impl fmt::Display for ServeError {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 ServeError::DataDir(e) => write!(f, "{e}"),
46 ServeError::MasterKey(e) => write!(f, "the master key cannot be read: {e:?}"),
47 ServeError::WrongMasterKey => {
48 f.write_str("the master key is not the one server.sqlite was made under")
49 }
50 ServeError::Registry(e) => write!(f, "server.sqlite cannot be opened: {e:?}"),
51 ServeError::KeysOutOfOrder(order) => {
52 let mut parts = Vec::new();
53 if order.wrapped_elsewhere > 0 {
54 parts.push(format!(
55 "{} tenant key(s) do not unwrap under the master key",
56 order.wrapped_elsewhere
57 ));
58 }
59 if order.rotations_stopped > 0 {
60 parts.push(format!(
61 "{} key rotation(s) stopped part way; run rekey tenant again",
62 order.rotations_stopped
63 ));
64 }
65 if order.data_under_other_keys > 0 {
66 parts.push(format!(
67 "{} tenant(s) with data under another key",
68 order.data_under_other_keys
69 ));
70 }
71 write!(
72 f,
73 "{}; vcon-server check names the tenants",
74 parts.join("; ")
75 )
76 }
77 ServeError::Tls(e) => write!(f, "the TLS certificate or key cannot be used: {e:?}"),
78 ServeError::Listen(e) => write!(f, "the listening address cannot be bound: {e}"),
79 ServeError::Serve(e) => write!(f, "serving failed: {e}"),
80 }
81 }
82}
83
84pub async fn run(config: &Config, shutdown: impl Future<Output = ()>) -> Result<(), ServeError> {
87 let _lock =
88 datadir::lock(&config.data_dir, Work::Serve.sharing()).map_err(ServeError::DataDir)?;
89 let master = Key::load_master(&config.master_key).map_err(ServeError::MasterKey)?;
90 let tenants = Tenants::open(
91 &config.data_dir,
92 master,
93 Arc::new(OsFs),
94 Arc::new(SystemClock),
95 )
96 .map_err(|e| match e {
97 TenantError::WrongKey => ServeError::WrongMasterKey,
98 other => ServeError::Registry(other),
99 })?;
100 let order = tenants.key_order().map_err(ServeError::Registry)?;
101 if !order.is_sound() {
102 return Err(ServeError::KeysOutOfOrder(order));
103 }
104 let tls = api::tls_config(&config.tls_cert, &config.tls_key).map_err(ServeError::Tls)?;
105 let listener = TcpListener::bind(config.listen)
106 .await
107 .map_err(ServeError::Listen)?;
108 let tenants = Arc::new(tenants);
109 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
112 let (swept, default) = (tenants.clone(), config.retention_default);
113 let sweeper = tokio::spawn(every(
114 config.purge_interval,
115 async {
116 let _ = stopped.await;
117 },
118 move || {
119 let tenants = swept.clone();
120 async move {
121 let _ = tokio::task::spawn_blocking(move || sweep(&tenants, default)).await;
122 }
123 },
124 ));
125 let (stop_warning, warning_stopped) = tokio::sync::oneshot::channel::<()>();
127 let (watched, rotation_warn) = (tenants.clone(), config.rotation_warn);
128 let warner = tokio::spawn(every(
129 ROTATION_CHECK,
130 async {
131 let _ = warning_stopped.await;
132 },
133 move || {
134 let tenants = watched.clone();
135 async move {
136 let _ = tokio::task::spawn_blocking(move || {
137 warn_if_rotation_overdue(&tenants, rotation_warn);
138 })
139 .await;
140 }
141 },
142 ));
143 let app = app_from(config, tenants);
144 tracing::info!(listen = %config.listen, "serving");
145 let served = api::serve(listener, tls, api::router(Arc::new(app)), shutdown).await;
146 let _ = stop.send(());
147 let _ = stop_warning.send(());
148 let _ = sweeper.await;
149 let _ = warner.await;
150 served.map_err(ServeError::Serve)?;
151 tracing::info!("stopped");
152 Ok(())
153}
154
155#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
157pub struct SweepReport {
158 pub purged: u64,
160 pub failed: u64,
162}
163
164pub fn sweep(tenants: &Tenants, default: Retention) -> SweepReport {
169 let mut report = SweepReport::default();
170 match tenants.ids() {
171 Ok(ids) => {
172 for id in ids {
173 let purged = tenants.retention(id).and_then(|own| {
174 let store = tenants.store(id)?;
175 Ok(store.purge_due(own.unwrap_or(default)))
176 });
177 match purged {
178 Ok(Ok(done)) => report.purged += done.purged,
179 _ => report.failed += 1,
180 }
181 }
182 }
183 Err(_) => report.failed += 1,
184 }
185 tracing::info!(
186 purged = report.purged,
187 failed = report.failed,
188 "purge sweep"
189 );
190 report
191}
192
193pub async fn every<F, Fut>(interval: Duration, shutdown: impl Future<Output = ()>, mut job: F)
196where
197 F: FnMut() -> Fut,
198 Fut: Future<Output = ()>,
199{
200 tokio::pin!(shutdown);
201 loop {
202 tokio::select! {
203 biased;
204 () = &mut shutdown => return,
205 () = job() => {}
206 }
207 tokio::select! {
208 biased;
209 () = &mut shutdown => return,
210 () = tokio::time::sleep(interval) => {}
211 }
212 }
213}
214
215fn app_from(config: &Config, tenants: Arc<Tenants>) -> App {
217 App {
218 tenants,
219 limits: Limits {
220 body: config.body_limit,
221 inflated: config.inflated_limit,
222 body_timeout: config.read_timeout,
223 },
224 supported: config.supported_critical.clone(),
225 retention_default: config.retention_default,
226 }
227}
228
229pub fn rotation_overdue(
232 backlog: &RotationBacklog,
233 rotation_warn: Duration,
234 now: OffsetDateTime,
235) -> bool {
236 [backlog.oldest_purge, backlog.oldest_removal]
237 .into_iter()
238 .flatten()
239 .any(|at| now - at > rotation_warn)
240}
241
242const ROTATION_CHECK: Duration = Duration::from_secs(86_400);
244
245fn warn_if_rotation_overdue(tenants: &Tenants, rotation_warn: Duration) {
247 match tenants.rotation_backlog() {
248 Ok(backlog) if rotation_overdue(&backlog, rotation_warn, SystemClock.now()) => {
249 tracing::warn!(
250 tenants_with_purges = backlog.tenants_with_purges,
251 purges = backlog.purges,
252 removals = backlog.removals,
253 "key rotation overdue: purged vCons and removed tenants stay readable on old copies until it"
254 );
255 }
256 Ok(_) => {}
257 Err(_) => tracing::warn!("the key rotation backlog could not be read"),
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use std::time::Duration;
264
265 use super::*;
266
267 mod startup {
268 use time::macros::datetime;
269
270 use super::*;
271
272 const NOW: OffsetDateTime = datetime!(2026-10-25 12:00 UTC);
273 const DAY: Duration = Duration::from_secs(86_400);
274
275 #[test]
276 fn rotation_is_overdue_once_the_oldest_waiting_work_passes_rotation_warn() {
277 let backlog =
278 |purge: Option<OffsetDateTime>, removal: Option<OffsetDateTime>| RotationBacklog {
279 tenants_with_purges: u64::from(purge.is_some()),
280 purges: u64::from(purge.is_some()),
281 oldest_purge: purge,
282 removals: u64::from(removal.is_some()),
283 oldest_removal: removal,
284 };
285 let day_ago = Some(datetime!(2026-10-24 12:00 UTC));
286 let older = Some(datetime!(2026-10-24 11:59:59 UTC));
287 for (b, overdue) in [
288 (backlog(None, None), false),
289 (backlog(day_ago, None), false),
290 (backlog(older, None), true),
291 (backlog(None, older), true),
292 (backlog(day_ago, older), true),
293 ] {
294 assert_eq!(rotation_overdue(&b, DAY, NOW), overdue, "{b:?}");
295 }
296 }
297
298 #[test]
299 fn a_refusal_for_keys_out_of_order_counts_and_points_at_check() {
300 let order = KeyOrder {
301 wrapped_elsewhere: 1,
302 rotations_stopped: 2,
303 data_under_other_keys: 3,
304 };
305 let text = ServeError::KeysOutOfOrder(order).to_string();
306 for words in [
307 "1 tenant key",
308 "2 key rotation",
309 "3 tenant",
310 "vcon-server check",
311 ] {
312 assert!(text.contains(words), "{words}: {text}");
313 }
314 }
315 }
316 use crate::retention::Retention;
317
318 #[test]
319 fn the_api_takes_its_limits_and_extensions_from_the_configuration() {
320 let dir = tempfile::tempdir().expect("temp dir");
321 let tenants = Arc::new(
322 Tenants::open(
323 dir.path(),
324 Key::generate().expect("key"),
325 Arc::new(OsFs),
326 Arc::new(SystemClock),
327 )
328 .expect("opens"),
329 );
330 let config = Config {
331 listen: "127.0.0.1:8443".parse().expect("address"),
332 tls_cert: "cert.pem".into(),
333 tls_key: "key.pem".into(),
334 data_dir: dir.path().to_owned(),
335 master_key: "master.key".into(),
336 retention_default: Retention::Months(18),
337 purge_interval: Duration::from_secs(3600),
338 rotation_warn: Duration::from_secs(86400),
339 body_limit: 12_345,
340 inflated_limit: 67_890,
341 read_timeout: Duration::from_secs(17),
342 supported_critical: ["known".to_owned()].into(),
343 };
344 let app = app_from(&config, tenants.clone());
345 assert!(Arc::ptr_eq(&app.tenants, &tenants));
346 assert_eq!(app.limits.body, 12_345);
347 assert_eq!(app.limits.inflated, 67_890);
348 assert_eq!(app.limits.body_timeout, Duration::from_secs(17));
349 assert_eq!(app.supported, config.supported_critical);
350 assert_eq!(app.retention_default, config.retention_default);
351 }
352
353 mod sweeps {
354 use std::sync::atomic::{AtomicUsize, Ordering};
355 use std::time::Duration;
356
357 use time::macros::datetime;
358 use uuid::Uuid;
359
360 use super::super::*;
361 use crate::audit::Actor;
362 use crate::ids::{KeyId, TenantId};
363 use crate::retention::Retention;
364 use crate::store::Caller;
365 use crate::store::testing::vcon;
366 use crate::tenants::fixture;
367
368 const CALLER: Caller = Caller { key: KeyId([7; 8]) };
369
370 fn deleted(tenants: &Tenants, id: TenantId, uuid: &str) -> Uuid {
373 let store = tenants.store(id).expect("its store");
374 let (found, body) = vcon(uuid, "kept");
375 store.create(&CALLER, &found, &body).expect("stored");
376 store
377 .delete(&CALLER, found.uuid, Retention::Years(7))
378 .expect("deleted");
379 found.uuid
380 }
381
382 fn kept(tenants: &Tenants, id: TenantId) -> u64 {
384 tenants
385 .store(id)
386 .expect("its store")
387 .counts()
388 .expect("counts")
389 .versions
390 }
391
392 #[test]
393 fn a_sweep_purges_what_is_due_in_each_tenant_under_its_own_period() {
394 let f = fixture();
395 let own = f
396 .tenants
397 .create(Actor::Cli, "own", Some(Retention::Days(1)))
398 .expect("made");
399 let default = f.tenants.create(Actor::Cli, "default", None).expect("made");
400 let long = f
401 .tenants
402 .create(Actor::Cli, "long", Some(Retention::Years(7)))
403 .expect("made");
404 deleted(&f.tenants, own, "0199e5c6-0d7a-7c11-8000-000000000001");
405 deleted(&f.tenants, default, "0199e5c6-0d7a-7c11-8000-000000000002");
406 deleted(&f.tenants, long, "0199e5c6-0d7a-7c11-8000-000000000003");
407 f.clock.set(datetime!(2026-10-25 13:00 UTC));
408
409 let report = sweep(&f.tenants, Retention::Days(1));
410
411 assert_eq!(
412 report,
413 SweepReport {
414 purged: 2,
415 failed: 0
416 }
417 );
418 assert_eq!(kept(&f.tenants, own), 0);
419 assert_eq!(kept(&f.tenants, default), 0);
420 assert_eq!(kept(&f.tenants, long), 1, "seven years have not passed");
421 }
422
423 #[test]
424 fn a_sweep_goes_on_past_a_tenant_it_cannot_sweep() {
425 let f = fixture();
426 let damaged = f.tenants.create(Actor::Cli, "damaged", None).expect("made");
427 let fine = f.tenants.create(Actor::Cli, "fine", None).expect("made");
428 deleted(&f.tenants, damaged, "0199e5c6-0d7a-7c11-8000-000000000001");
429 deleted(&f.tenants, fine, "0199e5c6-0d7a-7c11-8000-000000000002");
430 f.tenants
431 .conn_for_test()
432 .execute(
433 "UPDATE tenant SET retention = '7w' WHERE id = ?1",
434 [damaged.0],
435 )
436 .expect("damaged");
437 f.clock.set(datetime!(2026-10-25 13:00 UTC));
438
439 let report = sweep(&f.tenants, Retention::Days(1));
440
441 assert_eq!(
442 report,
443 SweepReport {
444 purged: 1,
445 failed: 1
446 }
447 );
448 assert_eq!(kept(&f.tenants, fine), 0);
449 assert_eq!(kept(&f.tenants, damaged), 1, "left for a later sweep");
450 }
451
452 #[tokio::test(start_paused = true)]
453 async fn every_runs_at_start_then_after_each_interval_until_shutdown() {
454 let runs = Arc::new(AtomicUsize::new(0));
455 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
456 let counted = runs.clone();
457 let task = tokio::spawn(every(
458 Duration::from_secs(60),
459 async move {
460 let _ = stopped.await;
461 },
462 move || {
463 counted.fetch_add(1, Ordering::SeqCst);
464 async {}
465 },
466 ));
467 let settle = || async {
468 for _ in 0..10 {
469 tokio::task::yield_now().await;
470 }
471 };
472
473 settle().await;
474 assert_eq!(runs.load(Ordering::SeqCst), 1, "at start");
475 tokio::time::advance(Duration::from_secs(59)).await;
476 settle().await;
477 assert_eq!(runs.load(Ordering::SeqCst), 1, "not before the interval");
478 tokio::time::advance(Duration::from_secs(1)).await;
479 settle().await;
480 assert_eq!(runs.load(Ordering::SeqCst), 2, "after the interval");
481
482 stop.send(()).expect("still running");
483 task.await.expect("ends on shutdown");
484 tokio::time::advance(Duration::from_secs(600)).await;
485 assert_eq!(runs.load(Ordering::SeqCst), 2, "none after shutdown");
486 }
487 }
488}