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::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 )
127 .map_err(|_| ())?,
128 );
129 let accounts = Arc::new(K1Accounts::open(Arc::clone(&invites)).map_err(|_| ())?);
130 let groups = Arc::new(
131 K1Groups::open(
132 &state_root.join("groups"),
133 Arc::clone(&ordering),
134 Arc::clone(&peering),
135 )
136 .map_err(|_| ())?,
137 );
138 let profiles = Arc::new(
139 K1AccessProfiles::open(
140 &state_root.join("access-profiles"),
141 Arc::clone(&ordering),
142 Arc::clone(&peering),
143 )
144 .map_err(|_| ())?,
145 );
146 let replay = ReplayWindow::open(ReplayConfig {
147 epoch_file: files.replay_epoch_path().to_owned(),
148 max_nonces_per_epoch: usize::MAX,
149 })
150 .await
151 .map_err(|_| ())?;
152 let unused_invites =
153 kcode_k1_daemon_invite_stock::reconcile(&invites, files.invite_links_path(), PUBLIC_ORIGIN)
154 .map_err(|_| ())?;
155 if unused_invites < 100 {
156 return Err(());
157 }
158 let accounts_for_people = Arc::clone(&accounts);
159 let adapter = K1HttpAccounts::new(accounts, Arc::clone(&invites));
160 let people = K1HttpPeople::new(accounts_for_people, groups, profiles);
161 let http = K1Http::new(
162 Config {
163 server_id: files.server_id().to_owned(),
164 public_origin: PUBLIC_ORIGIN.to_owned(),
165 max_body_bytes: usize::MAX,
166 },
167 replay,
168 adapter.identity_provider(),
169 )
170 .map_err(|_| ())?;
171 let authenticated = adapter
172 .authenticated_routes()
173 .merge(people.authenticated_routes())
174 .fallback(api_not_found);
175 let api = http
176 .router(
177 adapter.registration_endpoint(),
178 kcode_k1_terms::endpoint(),
179 authenticated,
180 )
181 .layer(middleware::from_fn(contextualize_api_error));
182 let config = PublicConfig {
183 protocol: "K1-HTTP-1",
184 server_id: files.server_id().to_owned(),
185 public_origin: PUBLIC_ORIGIN,
186 };
187 let config_route = get(move || {
188 let config = config.clone();
189 async move { ([(CACHE_CONTROL, "no-store")], Json(config)) }
190 });
191 let app = Router::new()
192 .route("/config.json", config_route)
193 .merge(api)
194 .layer(middleware::from_fn(require_authority));
195 Ok(Prepared {
196 app,
197 listener: TcpListener::bind(LISTEN_ADDRESS).await.map_err(|_| ())?,
198 signals: Signals::install()?,
199 unused_invites,
200 })
201}
202
203fn state_root(k1_root: &Path) -> PathBuf {
204 k1_root.join("state")
205}
206
207async fn api_not_found() -> Response {
208 json_error(
209 StatusCode::NOT_FOUND,
210 "not_found",
211 "authenticated API route not found",
212 )
213}
214
215async fn contextualize_api_error(request: Request, next: Next) -> Response {
216 let response = next.run(request).await;
217 if !(response.status().is_client_error() || response.status().is_server_error()) {
218 return response;
219 }
220 let (mut parts, body) = response.into_parts();
221 let bytes = match to_bytes(body, usize::MAX).await {
222 Ok(bytes) => bytes,
223 Err(_) => return Response::from_parts(parts, Body::empty()),
224 };
225 let Some(contextualized) = contextualize_error_body(&bytes) else {
226 return Response::from_parts(parts, Body::from(bytes));
227 };
228 parts.headers.remove(CONTENT_LENGTH);
229 Response::from_parts(parts, Body::from(contextualized))
230}
231
232fn contextualize_error_body(bytes: &[u8]) -> Option<Vec<u8>> {
233 let mut payload: Value = serde_json::from_slice(bytes).ok()?;
234 let object = payload.as_object_mut()?;
235 let code = object.get("error")?.as_str()?.to_owned();
236 let source = object
237 .get("message")
238 .and_then(Value::as_str)
239 .map(str::to_owned)
240 .unwrap_or_else(|| format!("error code {code}"));
241 object.insert(
242 "message".to_owned(),
243 Value::String(format!("{API_OPERATION}: {source}")),
244 );
245 Some(payload.to_string().into_bytes())
246}
247
248async fn require_authority(request: Request, next: Next) -> Response {
249 let mut values = request.headers().get_all(HOST).iter();
250 if values
251 .next()
252 .is_some_and(|value| value.as_bytes() == AUTHORITY.as_bytes())
253 && values.next().is_none()
254 {
255 next.run(request).await
256 } else {
257 json_error(
258 StatusCode::MISDIRECTED_REQUEST,
259 "invalid_request_authority",
260 "validate request authority: request authority is invalid",
261 )
262 }
263}
264
265fn json_error(status: StatusCode, code: &'static str, message: &'static str) -> Response {
266 let mut response = Response::new(Body::from(
267 serde_json::json!({"error": code, "message": message}).to_string(),
268 ));
269 *response.status_mut() = status;
270 response
271 .headers_mut()
272 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
273 response
274 .headers_mut()
275 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
276 response
277}
278
279fn write_readiness(unused_invites: usize) -> Result<(), ()> {
280 let stdout = std::io::stdout();
281 let mut output = stdout.lock();
282 serde_json::to_writer(
283 &mut output,
284 &Ready {
285 event: "ready",
286 public_origin: PUBLIC_ORIGIN,
287 unused_invites,
288 },
289 )
290 .map_err(|_| ())?;
291 output.write_all(b"\n").map_err(|_| ())?;
292 output.flush().map_err(|_| ())
293}
294
295fn warn_if_slow(elapsed: Duration, outcome: &'static str) {
296 if elapsed > STARTUP_BOUND {
297 eprintln!(
298 "{{\"module\":\"kcode-k1-daemon\",\"operation\":\"startup\",\"elapsed_us\":{},\"outcome\":\"{outcome}\"}}",
299 elapsed.as_micros()
300 );
301 }
302}
303
304impl Signals {
305 fn install() -> Result<Self, ()> {
306 Ok(Self {
307 interrupt: signal(SignalKind::interrupt()).map_err(|_| ())?,
308 terminate: signal(SignalKind::terminate()).map_err(|_| ())?,
309 })
310 }
311
312 async fn wait(mut self) {
313 tokio::select! {
314 _ = self.interrupt.recv() => {}
315 _ = self.terminate.recv() => {}
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn existing_child_message_is_preserved_under_daemon_context() {
326 let body = contextualize_error_body(
327 br#"{"error":"group_failed","message":"load group: child failure","detail":7}"#,
328 )
329 .unwrap();
330 let payload: Value = serde_json::from_slice(&body).unwrap();
331 assert_eq!(payload["error"], "group_failed");
332 assert_eq!(payload["detail"], 7);
333 assert_eq!(
334 payload["message"],
335 "serve API request: load group: child failure"
336 );
337 }
338
339 #[test]
340 fn missing_child_message_is_derived_from_stable_code() {
341 let body = contextualize_error_body(br#"{"error":"invalid_signature"}"#).unwrap();
342 let payload: Value = serde_json::from_slice(&body).unwrap();
343 assert_eq!(payload["error"], "invalid_signature");
344 assert_eq!(
345 payload["message"],
346 "serve API request: error code invalid_signature"
347 );
348 }
349
350 #[test]
351 fn supplied_root_maps_only_to_state() {
352 assert_eq!(
353 state_root(Path::new("/trusted/k1")),
354 PathBuf::from("/trusted/k1/state")
355 );
356 }
357}