1use std::future::Future;
27use std::net::SocketAddr;
28use std::sync::atomic::{AtomicI64, Ordering};
29use std::sync::{Arc, PoisonError, RwLock};
30use std::time::{SystemTime, UNIX_EPOCH};
31
32use anyhow::{Context, Result};
33use axum::extract::DefaultBodyLimit;
34use axum::middleware::{from_fn, from_fn_with_state};
37use axum::routing::{get, post};
38use axum::Router;
39use recall_wire::devices as paths;
40use recall_wire::MergeError;
41use tokio::net::TcpListener;
42use tokio::task::JoinHandle;
43
44use crate::config::TlsMode;
45use crate::merge::{Merger, Status};
46use crate::{format_timestamp, now, Config, Store};
47
48mod auth;
49mod devices;
50mod handlers;
51mod limit;
52mod middleware;
53mod respond;
54mod tls;
55
56use auth::ReplayCache;
57use devices::{
58 handle_approve, handle_create_authkey, handle_deny, handle_enroll, handle_list_authkeys,
59 handle_list_devices, handle_me, handle_pending, handle_poll, handle_revoke_authkey,
60 handle_revoke_device,
61};
62use handlers::{
63 handle_admin_page, handle_admin_stats, handle_discovery, handle_health, handle_pull,
64 handle_push, not_found,
65};
66use limit::RateLimiter;
67use middleware::{admin_only, guard, limited};
68
69const SWEEP_EVERY: std::time::Duration = std::time::Duration::from_secs(10 * 60);
73
74const MAX_BODY_BYTES: usize = 5 << 20;
77
78const ENROLL_BODY_BYTES: usize = 8 << 10;
82
83struct Runtime {
84 last_backup_at: String,
85 last_merge_at: String,
86 last_merge_error: Option<MergeError>,
87 claude_status: Status,
88}
89
90struct AppState {
91 cfg: Config,
92 store: Arc<Store>,
93 merger: Merger,
94 started_at: String,
95 started_unix: AtomicI64,
100 clock_offset: AtomicI64,
102 runtime: RwLock<Runtime>,
103 limiter: RateLimiter,
104 replay: ReplayCache,
105}
106
107impl AppState {
108 fn read(&self) -> std::sync::RwLockReadGuard<'_, Runtime> {
109 self.runtime.read().unwrap_or_else(PoisonError::into_inner)
110 }
111 fn write(&self) -> std::sync::RwLockWriteGuard<'_, Runtime> {
112 self.runtime.write().unwrap_or_else(PoisonError::into_inner)
113 }
114
115 fn now(&self) -> i64 {
117 unix_now() + self.clock_offset.load(Ordering::Relaxed)
118 }
119
120 fn started(&self) -> i64 {
122 self.started_unix.load(Ordering::Relaxed)
123 }
124}
125
126fn unix_now() -> i64 {
127 SystemTime::now()
128 .duration_since(UNIX_EPOCH)
129 .map(|d| d.as_secs() as i64)
130 .unwrap_or(0)
131}
132
133fn nonces_per_device(cfg: &Config) -> usize {
137 let live_ms = auth::NONCE_LIFETIME as u128 * 1000;
138 let window_ms = cfg.rate_limit_window.as_millis().max(1);
139 let windows = live_ms.div_ceil(window_ms);
140 (cfg.rate_limit_max as u128 * windows).min(usize::MAX as u128) as usize
141}
142
143pub struct Server {
145 state: Arc<AppState>,
146}
147
148impl Server {
149 pub fn new(cfg: Config, store: Arc<Store>) -> Self {
151 let limiter = RateLimiter::new(cfg.rate_limit_window, cfg.rate_limit_max);
152 let merger = Merger::new(cfg.claude_bin.clone(), cfg.merge_timeout);
153 let replay = ReplayCache::new(auth::WINDOW, nonces_per_device(&cfg));
154 Self {
155 state: Arc::new(AppState {
156 cfg,
157 store,
158 merger,
159 started_at: now(),
160 started_unix: AtomicI64::new(unix_now()),
161 clock_offset: AtomicI64::new(0),
162 runtime: RwLock::new(Runtime {
163 last_backup_at: String::new(),
164 last_merge_at: String::new(),
165 last_merge_error: None,
166 claude_status: Status::default(),
167 }),
168 limiter,
169 replay,
170 }),
171 }
172 }
173
174 pub fn router(&self) -> Router {
177 let state = self.state.clone();
178 let admin = Router::new()
182 .route("/admin/stats", get(handle_admin_stats).fallback(not_found))
183 .route(
184 paths::DEVICES_PATH,
185 get(handle_list_devices).fallback(not_found),
186 )
187 .route(
188 paths::APPROVE_PATH,
189 post(handle_approve).fallback(not_found),
190 )
191 .route(paths::DENY_PATH, post(handle_deny).fallback(not_found))
192 .route(
193 "/v1/devices/pending/{user_code}",
194 get(handle_pending).fallback(not_found),
195 )
196 .route(
197 "/v1/devices/{id}/revoke",
198 post(handle_revoke_device).fallback(not_found),
199 )
200 .route(
201 paths::AUTHKEYS_PATH,
202 get(handle_list_authkeys)
203 .post(handle_create_authkey)
204 .fallback(not_found),
205 )
206 .route(
207 "/v1/authkeys/{id}/revoke",
208 post(handle_revoke_authkey).fallback(not_found),
209 )
210 .route_layer(from_fn(admin_only))
211 .route_layer(from_fn_with_state(state.clone(), guard));
212 let enrolment = Router::new()
217 .route(paths::ENROLL_PATH, post(handle_enroll).fallback(not_found))
218 .route(
219 paths::ENROLL_POLL_PATH,
220 post(handle_poll).fallback(not_found),
221 )
222 .route_layer(DefaultBodyLimit::max(ENROLL_BODY_BYTES))
223 .route_layer(from_fn_with_state(state.clone(), limited));
224 Router::new()
225 .route(
230 "/sync",
231 get(handle_pull).post(handle_push).fallback(not_found),
232 )
233 .route(paths::DEVICES_ME_PATH, get(handle_me).fallback(not_found))
234 .route_layer(from_fn_with_state(state.clone(), guard))
237 .merge(admin)
238 .merge(enrolment)
239 .route("/health", get(handle_health).fallback(not_found))
240 .route(
241 recall_wire::DISCOVERY_PATH,
242 get(handle_discovery).fallback(not_found),
243 )
244 .route("/admin", get(handle_admin_page).fallback(not_found))
245 .fallback(not_found)
246 .layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
247 .with_state(state)
248 }
249
250 pub async fn refresh_claude_status(&self) {
252 let status = self.state.merger.check_status().await;
253 self.state.write().claude_status = status;
254 }
255
256 pub fn claude_status(&self) -> Status {
258 self.state.read().claude_status.clone()
259 }
260
261 pub fn set_claude_status(&self, status: Status) {
266 self.state.write().claude_status = status;
267 }
268
269 pub fn set_clock_offset(&self, seconds: i64) {
274 self.state.clock_offset.store(seconds, Ordering::Relaxed);
275 }
276
277 pub fn backdate_start(&self, seconds: i64) {
283 self.state
284 .started_unix
285 .fetch_sub(seconds, Ordering::Relaxed);
286 }
287
288 pub fn run_backup(&self) {
291 run_backup(&self.state);
292 }
293
294 pub fn sweep_devices(&self) -> Result<(usize, usize)> {
298 sweep_devices(&self.state)
299 }
300
301 pub fn start_background(&self) -> Vec<JoinHandle<()>> {
305 let mut tasks = Vec::new();
306 {
307 let state = self.state.clone();
308 tasks.push(tokio::spawn(async move {
309 loop {
310 let s = state.clone();
311 match tokio::task::spawn_blocking(move || sweep_devices(&s)).await {
312 Ok(Ok((0, 0))) => {}
313 Ok(Ok((devices, enrollments))) => eprintln!(
314 "removed {devices} idle ephemeral devices and {enrollments} expired enrolments"
315 ),
316 Ok(Err(e)) => eprintln!("device sweep failed: {e:#}"),
317 Err(_) => {}
318 }
319 tokio::time::sleep(SWEEP_EVERY).await;
320 }
321 }));
322 }
323 if self.state.cfg.merge_enabled {
324 let state = self.state.clone();
325 tasks.push(tokio::spawn(async move {
326 let every = state.cfg.claude_status_interval;
327 loop {
328 let status = state.merger.check_status().await;
329 state.write().claude_status = status;
330 tokio::time::sleep(every).await;
331 }
332 }));
333 }
334 if !self.state.cfg.backup_dir.is_empty() {
335 let state = self.state.clone();
336 tasks.push(tokio::spawn(async move {
337 let every = state.cfg.backup_interval;
338 loop {
339 let s = state.clone();
343 let _ = tokio::task::spawn_blocking(move || run_backup(&s)).await;
344 tokio::time::sleep(every).await;
345 }
346 }));
347 }
348 tasks
349 }
350
351 pub async fn serve(&self) -> Result<()> {
354 let listener = TcpListener::bind(&self.state.cfg.addr)
355 .await
356 .with_context(|| format!("binding {}", self.state.cfg.addr))?;
357 self.serve_with_shutdown(listener, shutdown_signal()).await
358 }
359
360 pub async fn serve_with_shutdown<F>(&self, listener: TcpListener, shutdown: F) -> Result<()>
364 where
365 F: Future<Output = ()> + Send + 'static,
366 {
367 let transport = match &self.state.cfg.tls {
372 TlsMode::Off => None,
373 mode => Some(tls::prepare(mode).await?),
374 };
375 eprintln!(
376 "recall server listening on {} ({}, db: {})",
377 listener
378 .local_addr()
379 .map_or_else(|_| self.state.cfg.addr.clone(), |a| a.to_string()),
380 transport
381 .as_ref()
382 .map_or("plain http", tls::Prepared::description),
383 self.state.cfg.db_path
384 );
385 let tasks = self.start_background();
386 let result = match transport {
387 None => axum::serve(
388 listener,
389 self.router()
390 .into_make_service_with_connect_info::<SocketAddr>(),
391 )
392 .with_graceful_shutdown(shutdown)
393 .await
394 .map_err(Into::into),
395 Some(prepared) => {
396 let listener = listener.into_std().context("preparing the TLS listener")?;
402 let limits = tls::Limits::from_config(&self.state.cfg);
403 tls::serve(self.router(), listener, prepared, limits, shutdown).await
404 }
405 };
406 for task in tasks {
407 task.abort();
408 }
409 result
410 }
411}
412
413fn sweep_devices(state: &AppState) -> Result<(usize, usize)> {
414 let now = time::OffsetDateTime::now_utc();
415 state.store.sweep_devices(
416 &format_timestamp(now - state.cfg.ephemeral_device_ttl),
417 &format_timestamp(now - devices::EXPIRED_ENROLLMENT_KEPT),
418 )
419}
420
421fn run_backup(state: &AppState) {
422 if state.cfg.backup_dir.is_empty() {
423 return;
424 }
425 match state
426 .store
427 .backup(&state.cfg.backup_dir, state.cfg.backup_keep)
428 {
429 Ok(dest) => {
430 state.write().last_backup_at = now();
431 eprintln!("backup written: {}", dest.display());
432 }
433 Err(e) => eprintln!("backup failed: {e:#}"),
434 }
435}
436
437async fn shutdown_signal() {
438 let ctrl_c = async {
439 let _ = tokio::signal::ctrl_c().await;
440 };
441 #[cfg(unix)]
442 let terminate = async {
443 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
444 Ok(mut sig) => {
445 sig.recv().await;
446 }
447 Err(_) => std::future::pending::<()>().await,
448 }
449 };
450 #[cfg(not(unix))]
451 let terminate = std::future::pending::<()>();
452
453 tokio::select! {
454 _ = ctrl_c => {}
455 _ = terminate => {}
456 }
457}