1#![doc = include_str!("../Documentation.md")]
2
3use axum::body::{Body, to_bytes};
4use axum::extract::Request;
5use axum::http::header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, HOST};
6use axum::http::{HeaderValue, StatusCode};
7use axum::middleware::{self, Next};
8use axum::response::Response;
9use axum::routing::get;
10use axum::{Json, Router};
11use kcode_k1_access_profiles::K1AccessProfiles;
12use kcode_k1_accounts::K1Accounts;
13use kcode_k1_daemon_files::DaemonFiles;
14use kcode_k1_groups::K1Groups;
15use kcode_k1_http::{Config, K1Http};
16use kcode_k1_http_accounts::K1HttpAccounts;
17use kcode_k1_http_people::K1HttpPeople;
18use kcode_k1_http_replay::{ReplayConfig, ReplayWindow};
19use kcode_k1_invites::{InviteVerifierKey, K1Invites};
20use kcode_k1_peering::K1Peering;
21use kcode_k1_txn_ordering::K1TxnOrdering;
22use serde::Serialize;
23use serde_json::Value;
24use std::io::Write as _;
25use std::path::{Path, PathBuf};
26use std::process::ExitCode;
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29use tokio::net::TcpListener;
30use tokio::signal::unix::{Signal, SignalKind, signal};
31
32const LISTEN_ADDRESS: &str = "127.0.0.1:4450";
33const PUBLIC_ORIGIN: &str = "http://localhost:4450";
34const AUTHORITY: &str = "localhost:4450";
35const STARTUP_BOUND: Duration = Duration::from_millis(100);
36const API_OPERATION: &str = "serve API request";
37
38#[derive(Clone, Serialize)]
39struct PublicConfig {
40 protocol: &'static str,
41 server_id: String,
42 public_origin: &'static str,
43}
44
45#[derive(Serialize)]
46struct Ready {
47 event: &'static str,
48 public_origin: &'static str,
49 unused_invites: usize,
50}
51
52struct Prepared {
53 app: Router,
54 listener: TcpListener,
55 signals: Signals,
56 unused_invites: usize,
57}
58
59struct Signals {
60 interrupt: Signal,
61 terminate: Signal,
62}
63
64pub fn run(k1_root: PathBuf) -> ExitCode {
66 let runtime = match tokio::runtime::Builder::new_multi_thread()
67 .enable_all()
68 .build()
69 {
70 Ok(runtime) => runtime,
71 Err(_) => {
72 eprintln!("kcode-k1-daemon: startup failed");
73 return ExitCode::from(1);
74 }
75 };
76 runtime.block_on(run_async(k1_root))
77}
78
79async fn run_async(k1_root: PathBuf) -> ExitCode {
80 let started = Instant::now();
81 let prepared = match startup(k1_root).await {
82 Ok(prepared) => prepared,
83 Err(()) => {
84 warn_if_slow(started.elapsed(), "error");
85 eprintln!("kcode-k1-daemon: startup failed");
86 return ExitCode::from(1);
87 }
88 };
89 let elapsed = started.elapsed();
90 if write_readiness(prepared.unused_invites).is_err() {
91 warn_if_slow(elapsed, "error");
92 eprintln!("kcode-k1-daemon: startup failed");
93 return ExitCode::from(1);
94 }
95 warn_if_slow(elapsed, "ready");
96 let Prepared {
97 app,
98 listener,
99 signals,
100 ..
101 } = prepared;
102 match axum::serve(listener, app)
103 .with_graceful_shutdown(signals.wait())
104 .await
105 {
106 Ok(()) => ExitCode::SUCCESS,
107 Err(_) => {
108 eprintln!("kcode-k1-daemon: listener failed");
109 ExitCode::from(1)
110 }
111 }
112}
113
114async fn startup(k1_root: PathBuf) -> Result<Prepared, ()> {
115 let state_root = state_root(&k1_root);
116 let files = DaemonFiles::open(&state_root).map_err(|_| ())?;
117 let ordering = Arc::new(K1TxnOrdering::open(&state_root.join("ordering")).map_err(|_| ())?);
118 let peering = Arc::new(
119 K1Peering::open(&state_root.join("peering"), Arc::clone(&ordering)).map_err(|_| ())?,
120 );
121 let invites = Arc::new(
122 K1Invites::open(
123 &state_root.join("invites"),
124 Arc::clone(&ordering),
125 Arc::clone(&peering),
126 InviteVerifierKey::from_bytes(files.invite_verifier_key()),
127 )
128 .map_err(|_| ())?,
129 );
130 let accounts = Arc::new(K1Accounts::open(Arc::clone(&invites)).map_err(|_| ())?);
131 let groups = Arc::new(
132 K1Groups::open(
133 &state_root.join("groups"),
134 Arc::clone(&ordering),
135 Arc::clone(&peering),
136 )
137 .map_err(|_| ())?,
138 );
139 let profiles = Arc::new(
140 K1AccessProfiles::open(
141 &state_root.join("access-profiles"),
142 Arc::clone(&ordering),
143 Arc::clone(&peering),
144 )
145 .map_err(|_| ())?,
146 );
147 let replay = ReplayWindow::open(ReplayConfig {
148 epoch_file: files.replay_file().to_owned(),
149 max_nonces_per_epoch: usize::MAX,
150 })
151 .await
152 .map_err(|_| ())?;
153 let unused_invites =
154 kcode_k1_daemon_invite_stock::reconcile(&invites, files.invite_links_file(), PUBLIC_ORIGIN)
155 .map_err(|_| ())?;
156 if unused_invites < 100 {
157 return Err(());
158 }
159 let accounts_for_people = Arc::clone(&accounts);
160 let adapter = K1HttpAccounts::new(accounts, Arc::clone(&invites));
161 let people = K1HttpPeople::new(accounts_for_people, groups, profiles);
162 let http = K1Http::new(
163 Config {
164 server_id: files.server_id().to_owned(),
165 public_origin: PUBLIC_ORIGIN.to_owned(),
166 max_body_bytes: usize::MAX,
167 },
168 replay,
169 adapter.identity_provider(),
170 )
171 .map_err(|_| ())?;
172 let authenticated = adapter
173 .authenticated_routes()
174 .merge(people.authenticated_routes())
175 .fallback(api_not_found);
176 let api = http
177 .router(
178 adapter.registration_endpoint(),
179 kcode_k1_terms::endpoint(),
180 authenticated,
181 )
182 .layer(middleware::from_fn(contextualize_api_error));
183 let config = PublicConfig {
184 protocol: "K1-HTTP-1",
185 server_id: files.server_id().to_owned(),
186 public_origin: PUBLIC_ORIGIN,
187 };
188 let config_route = get(move || {
189 let config = config.clone();
190 async move { ([(CACHE_CONTROL, "no-store")], Json(config)) }
191 });
192 let app = Router::new()
193 .route("/config.json", config_route)
194 .merge(api)
195 .layer(middleware::from_fn(require_authority));
196 Ok(Prepared {
197 app,
198 listener: TcpListener::bind(LISTEN_ADDRESS).await.map_err(|_| ())?,
199 signals: Signals::install()?,
200 unused_invites,
201 })
202}
203
204fn state_root(k1_root: &Path) -> PathBuf {
205 k1_root.join("state")
206}
207
208async fn api_not_found() -> Response {
209 json_error(
210 StatusCode::NOT_FOUND,
211 "not_found",
212 "authenticated API route not found",
213 )
214}
215
216async fn contextualize_api_error(request: Request, next: Next) -> Response {
217 let response = next.run(request).await;
218 if !(response.status().is_client_error() || response.status().is_server_error()) {
219 return response;
220 }
221 let (mut parts, body) = response.into_parts();
222 let bytes = match to_bytes(body, usize::MAX).await {
223 Ok(bytes) => bytes,
224 Err(_) => return Response::from_parts(parts, Body::empty()),
225 };
226 let Some(contextualized) = contextualize_error_body(&bytes) else {
227 return Response::from_parts(parts, Body::from(bytes));
228 };
229 parts.headers.remove(CONTENT_LENGTH);
230 Response::from_parts(parts, Body::from(contextualized))
231}
232
233fn contextualize_error_body(bytes: &[u8]) -> Option<Vec<u8>> {
234 let mut payload: Value = serde_json::from_slice(bytes).ok()?;
235 let object = payload.as_object_mut()?;
236 let code = object.get("error")?.as_str()?.to_owned();
237 let source = object
238 .get("message")
239 .and_then(Value::as_str)
240 .map(str::to_owned)
241 .unwrap_or_else(|| format!("error code {code}"));
242 object.insert(
243 "message".to_owned(),
244 Value::String(format!("{API_OPERATION}: {source}")),
245 );
246 Some(payload.to_string().into_bytes())
247}
248
249async fn require_authority(request: Request, next: Next) -> Response {
250 let mut values = request.headers().get_all(HOST).iter();
251 if values
252 .next()
253 .is_some_and(|value| value.as_bytes() == AUTHORITY.as_bytes())
254 && values.next().is_none()
255 {
256 next.run(request).await
257 } else {
258 json_error(
259 StatusCode::MISDIRECTED_REQUEST,
260 "invalid_request_authority",
261 "validate request authority: request authority is invalid",
262 )
263 }
264}
265
266fn json_error(status: StatusCode, code: &'static str, message: &'static str) -> Response {
267 let mut response = Response::new(Body::from(
268 serde_json::json!({"error": code, "message": message}).to_string(),
269 ));
270 *response.status_mut() = status;
271 response
272 .headers_mut()
273 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
274 response
275 .headers_mut()
276 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
277 response
278}
279
280fn write_readiness(unused_invites: usize) -> Result<(), ()> {
281 let stdout = std::io::stdout();
282 let mut output = stdout.lock();
283 serde_json::to_writer(
284 &mut output,
285 &Ready {
286 event: "ready",
287 public_origin: PUBLIC_ORIGIN,
288 unused_invites,
289 },
290 )
291 .map_err(|_| ())?;
292 output.write_all(b"\n").map_err(|_| ())?;
293 output.flush().map_err(|_| ())
294}
295
296fn warn_if_slow(elapsed: Duration, outcome: &'static str) {
297 if elapsed > STARTUP_BOUND {
298 eprintln!(
299 "{{\"module\":\"kcode-k1-daemon\",\"operation\":\"startup\",\"elapsed_us\":{},\"outcome\":\"{outcome}\"}}",
300 elapsed.as_micros()
301 );
302 }
303}
304
305impl Signals {
306 fn install() -> Result<Self, ()> {
307 Ok(Self {
308 interrupt: signal(SignalKind::interrupt()).map_err(|_| ())?,
309 terminate: signal(SignalKind::terminate()).map_err(|_| ())?,
310 })
311 }
312
313 async fn wait(mut self) {
314 tokio::select! {
315 _ = self.interrupt.recv() => {}
316 _ = self.terminate.recv() => {}
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn existing_child_message_is_preserved_under_daemon_context() {
327 let body = contextualize_error_body(
328 br#"{"error":"group_failed","message":"load group: child failure","detail":7}"#,
329 )
330 .unwrap();
331 let payload: Value = serde_json::from_slice(&body).unwrap();
332 assert_eq!(payload["error"], "group_failed");
333 assert_eq!(payload["detail"], 7);
334 assert_eq!(
335 payload["message"],
336 "serve API request: load group: child failure"
337 );
338 }
339
340 #[test]
341 fn missing_child_message_is_derived_from_stable_code() {
342 let body = contextualize_error_body(br#"{"error":"invalid_signature"}"#).unwrap();
343 let payload: Value = serde_json::from_slice(&body).unwrap();
344 assert_eq!(payload["error"], "invalid_signature");
345 assert_eq!(
346 payload["message"],
347 "serve API request: error code invalid_signature"
348 );
349 }
350
351 #[test]
352 fn supplied_root_maps_only_to_state() {
353 assert_eq!(
354 state_root(Path::new("/trusted/k1")),
355 PathBuf::from("/trusted/k1/state")
356 );
357 }
358}