1use crate::engine::{Engine, EngineError, RegisterWorker, TaskAssignment};
5use crate::identity::{Identities, WorkerIdentity};
6use crate::model::TaskError;
7use axum::extract::{DefaultBodyLimit, Path, Query, Request, State};
8use axum::http::{HeaderMap, HeaderValue, StatusCode};
9use axum::middleware::{self, Next};
10use axum::response::{IntoResponse, Response};
11use axum::routing::{get, post};
12use axum::{Extension, Json, Router};
13use serde::Deserialize;
14use serde_json::{json, Value};
15use std::net::SocketAddr;
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18use tokio::sync::Notify;
19
20pub const API_VERSION: &str = "orbit-api/1";
21
22pub struct AppState {
23 pub engine: Mutex<Engine>,
24 pub changed: Arc<Notify>,
25 pub auth_token: Option<String>,
26 pub worker_token: Option<String>,
28 pub identities: Identities,
30}
31
32pub type Shared = Arc<AppState>;
33
34pub struct ApiError(pub(crate) EngineError);
35
36impl From<EngineError> for ApiError {
37 fn from(e: EngineError) -> Self {
38 ApiError(e)
39 }
40}
41
42impl IntoResponse for ApiError {
43 fn into_response(self) -> Response {
44 let status =
45 StatusCode::from_u16(self.0.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
46 (
47 status,
48 Json(json!({ "error": { "code": self.0.code, "message": self.0.message } })),
49 )
50 .into_response()
51 }
52}
53
54fn bad_request(msg: impl Into<String>) -> ApiError {
55 ApiError(EngineError::new(400, "INVALID_REQUEST", msg))
56}
57
58type ApiResult<T> = Result<T, ApiError>;
59
60pub(crate) fn engine(state: &Shared) -> std::sync::MutexGuard<'_, Engine> {
61 state.engine.lock().unwrap_or_else(|p| p.into_inner())
62}
63
64pub fn router(state: Shared, body_limit: usize) -> Router {
65 Router::new()
66 .route("/v1/health", get(health))
67 .route("/v1/metrics", get(metrics))
68 .route("/v1/workflows", post(register_workflow).get(list_workflows))
69 .route("/v1/workflows/{name}", get(get_workflow_latest))
70 .route("/v1/workflows/{name}/{version}", get(get_workflow))
71 .route("/v1/executions", post(start_execution).get(list_executions))
72 .route("/v1/executions/{id}", get(get_execution))
73 .route("/v1/executions/{id}/history", get(get_history))
74 .route("/v1/executions/{id}/cancel", post(cancel_execution))
75 .route("/v1/executions/{id}/signals/{name}", post(send_signal))
76 .route("/v1/executions/{id}/queries/{name}", get(run_query))
77 .route("/v1/executions/{id}/wait", get(wait_execution))
78 .route("/v1/workers", get(list_workers))
79 .route("/v1/workers/register", post(register_worker))
80 .route("/v1/workers/heartbeat", post(heartbeat))
81 .route("/v1/workers/poll", post(poll))
82 .route("/v1/workers/unregister", post(unregister))
83 .route("/v1/tasks/complete", post(complete_task))
84 .route("/v1/tasks/fail", post(fail_task))
85 .layer(middleware::from_fn_with_state(state.clone(), auth))
86 .layer(DefaultBodyLimit::max(body_limit))
87 .with_state(state)
88}
89
90fn is_worker_path(path: &str) -> bool {
92 path.starts_with("/v1/workers/") || path.starts_with("/v1/tasks/")
93}
94
95#[derive(Clone)]
97pub enum Caller {
98 Open,
100 Admin,
101 SharedWorker,
103 Worker(Arc<WorkerIdentity>),
105 Client(Arc<crate::identity::ClientIdentity>),
108}
109
110impl Caller {
111 fn identity(&self) -> Option<&WorkerIdentity> {
112 match self {
113 Caller::Worker(w) => Some(w),
114 _ => None,
115 }
116 }
117}
118
119fn forbidden(msg: impl Into<String>) -> ApiError {
120 ApiError(EngineError::new(403, "FORBIDDEN", msg))
121}
122
123fn allow(caller: &Caller, action: &str, workflow: &str) -> ApiResult<()> {
125 if let Caller::Client(c) = caller {
126 if !c.allows(action, workflow) {
127 return Err(forbidden(format!(
128 "client `{}` may not {action} executions of `{workflow}`",
129 c.id
130 )));
131 }
132 }
133 Ok(())
134}
135
136fn allow_definition(caller: &Caller, workflow: &str) -> ApiResult<()> {
138 if let Caller::Client(c) = caller {
139 if !c.can_read(workflow) {
140 return Err(forbidden(format!(
141 "client `{}` may not read the definition of `{workflow}`",
142 c.id
143 )));
144 }
145 }
146 Ok(())
147}
148
149fn allow_execution(state: &Shared, caller: &Caller, action: &str, id: &str) -> ApiResult<()> {
151 if matches!(caller, Caller::Client(_)) {
152 let workflow = engine(state).execution(id)?.workflow;
153 allow(caller, action, &workflow)?;
154 }
155 Ok(())
156}
157
158pub(crate) fn identify(
171 state: &AppState,
172 bearer: Option<&str>,
173 cert_der: Option<&[u8]>,
174) -> Result<Option<Caller>, ApiError> {
175 let secured = state.auth_token.is_some() || !state.identities.is_empty();
176 if !secured {
177 return Ok(Some(Caller::Open));
178 }
179 if let Some(t) = bearer {
180 if state
181 .auth_token
182 .as_ref()
183 .is_some_and(|a| constant_time_eq(t.as_bytes(), a.as_bytes()))
184 {
185 return Ok(Some(Caller::Admin));
186 }
187 if let Some(w) = state.identities.by_token(t) {
188 return Ok(Some(Caller::Worker(Arc::new(w.clone()))));
189 }
190 if let Some(c) = state.identities.client_by_token(t) {
191 return Ok(Some(Caller::Client(Arc::new(c.clone()))));
192 }
193 if state
194 .worker_token
195 .as_ref()
196 .is_some_and(|w| constant_time_eq(t.as_bytes(), w.as_bytes()))
197 {
198 return Ok(Some(Caller::SharedWorker));
199 }
200 }
201 if let Some(der) = cert_der {
202 if let Some(w) = state.identities.by_certificate(der) {
203 return Ok(Some(Caller::Worker(Arc::new(w.clone()))));
204 }
205 return match state.identities.client_by_certificate(der) {
206 Some(c) => Ok(Some(Caller::Client(Arc::new(c.clone())))),
207 None => Err(forbidden(
208 "the client certificate is valid but matches no worker or client in the workers file",
209 )),
210 };
211 }
212 Ok(None)
213}
214
215async fn auth(State(state): State<Shared>, mut req: Request, next: Next) -> Response {
216 let path = req.uri().path().to_string();
217 let bearer = req
218 .headers()
219 .get("authorization")
220 .and_then(|v| v.to_str().ok())
221 .and_then(|v| v.strip_prefix("Bearer "))
222 .map(str::to_string);
223 let cert = req
224 .extensions()
225 .get::<Arc<axum_server::tls::TlsInfo>>()
226 .and_then(|info| info.peer_certificates.first().cloned());
227 let caller = match identify(&state, bearer.as_deref(), cert.as_deref()) {
228 Ok(Some(c)) => c,
229 Ok(None) if path == "/v1/health" => Caller::Open,
230 Ok(None) => {
231 return ApiError(EngineError::new(
232 401,
233 "UNAUTHENTICATED",
234 "missing or invalid credentials",
235 ))
236 .into_response()
237 }
238 Err(e) => return e.into_response(),
239 };
240 if matches!(caller, Caller::SharedWorker | Caller::Worker(_))
241 && !is_worker_path(&path)
242 && path != "/v1/health"
243 {
244 return forbidden("worker credentials may only call /v1/workers/* and /v1/tasks/*")
245 .into_response();
246 }
247 if let Caller::Client(c) = &caller {
248 let admin_only = (path == "/v1/workflows" && req.method() == axum::http::Method::POST)
249 || path.starts_with("/v1/workers")
250 || path == "/v1/metrics";
251 if is_worker_path(&path) || admin_only {
252 return forbidden(format!(
253 "client `{}` may only use the execution API (start, signal, query, read, cancel)",
254 c.id
255 ))
256 .into_response();
257 }
258 }
259 req.extensions_mut().insert(caller);
260 let mut resp = next.run(req).await;
261 resp.headers_mut()
262 .insert("x-orbit-api-version", HeaderValue::from_static(API_VERSION));
263 resp
264}
265
266pub(crate) fn authorize_session(state: &Shared, caller: &Caller, session: &str) -> ApiResult<()> {
268 if let Some(id) = caller.identity() {
269 if let Some(owner) = engine(state).session_worker_lookup(session) {
270 if owner != id.id {
271 return Err(forbidden(format!(
272 "session belongs to worker `{owner}`, not `{}`",
273 id.id
274 )));
275 }
276 }
277 }
278 Ok(())
279}
280
281pub(crate) fn authorize_token(state: &Shared, caller: &Caller, token: &str) -> ApiResult<()> {
283 if let Some(id) = caller.identity() {
284 if let Some(owner) = engine(state).token_worker_lookup(token) {
285 if owner != id.id {
286 return Err(forbidden(format!(
287 "task token was issued to worker `{owner}`, not `{}`",
288 id.id
289 )));
290 }
291 }
292 }
293 Ok(())
294}
295
296fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
297 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
298}
299
300async fn health(State(state): State<Shared>) -> Json<Value> {
301 let e = engine(&state);
302 Json(json!({
303 "status": "ok",
304 "api_version": API_VERSION,
305 "runtime_version": env!("CARGO_PKG_VERSION"),
306 "ir_versions": orbit_ir::SUPPORTED_IR_VERSIONS,
307 "active_executions": e.active_count(),
308 "worker_sessions": e.session_count(),
309 }))
310}
311
312async fn metrics(State(state): State<Shared>) -> Response {
313 let e = engine(&state);
314 let body = e.metrics.render(e.active_count(), e.session_count());
315 let mut headers = HeaderMap::new();
316 headers.insert(
317 "content-type",
318 HeaderValue::from_static("text/plain; version=0.0.4"),
319 );
320 (headers, body).into_response()
321}
322
323async fn register_workflow(State(state): State<Shared>, body: String) -> ApiResult<Response> {
324 let ir = orbit_ir::WorkflowIr::from_json(&body)
325 .map_err(|e| ApiError(EngineError::new(400, "INVALID_IR", e.0)))?;
326 let (name, version, hash) = (
327 ir.name.clone(),
328 ir.version.clone(),
329 ir.definition_hash.clone(),
330 );
331 let created = engine(&state).register_definition(ir)?;
332 let status = if created {
333 StatusCode::CREATED
334 } else {
335 StatusCode::OK
336 };
337 Ok((status, Json(json!({ "name": name, "version": version, "definition_hash": hash, "created": created }))).into_response())
338}
339
340fn summary(d: &orbit_ir::WorkflowIr) -> Value {
341 json!({
342 "name": d.name,
343 "version": d.version,
344 "description": d.description,
345 "definition_hash": d.definition_hash,
346 "steps": d.steps.iter().map(|s| s.id.clone()).collect::<Vec<_>>(),
347 "tasks": d.contracts.keys().collect::<Vec<_>>(),
348 })
349}
350
351async fn list_workflows(
352 State(state): State<Shared>,
353 Extension(caller): Extension<Caller>,
354) -> Json<Value> {
355 let defs = engine(&state).definitions();
356 let visible = |name: &str| match &caller {
357 Caller::Client(c) => c.can_read(name),
358 _ => true,
359 };
360 Json(
361 json!({ "workflows": defs.iter().filter(|d| visible(&d.name)).map(|d| summary(d)).collect::<Vec<_>>() }),
362 )
363}
364
365async fn get_workflow(
366 State(state): State<Shared>,
367 Extension(caller): Extension<Caller>,
368 Path((name, version)): Path<(String, String)>,
369) -> ApiResult<Json<Value>> {
370 allow_definition(&caller, &name)?;
371 let d = engine(&state)
372 .definition(&name, Some(&version))
373 .ok_or_else(|| {
374 ApiError(EngineError::new(
375 404,
376 "WORKFLOW_NOT_FOUND",
377 "workflow not found",
378 ))
379 })?;
380 Ok(Json(serde_json::to_value(&*d).unwrap_or_default()))
381}
382
383async fn get_workflow_latest(
384 State(state): State<Shared>,
385 Extension(caller): Extension<Caller>,
386 Path(name): Path<String>,
387) -> ApiResult<Json<Value>> {
388 allow_definition(&caller, &name)?;
389 let d = engine(&state).definition(&name, None).ok_or_else(|| {
390 ApiError(EngineError::new(
391 404,
392 "WORKFLOW_NOT_FOUND",
393 "workflow not found",
394 ))
395 })?;
396 Ok(Json(serde_json::to_value(&*d).unwrap_or_default()))
397}
398
399#[derive(Deserialize)]
400struct StartRequest {
401 workflow: String,
402 #[serde(default)]
403 version: Option<String>,
404 #[serde(default)]
405 input: Value,
406 #[serde(default)]
407 execution_id: Option<String>,
408}
409
410async fn start_execution(
411 State(state): State<Shared>,
412 Extension(caller): Extension<Caller>,
413 Json(req): Json<StartRequest>,
414) -> ApiResult<Response> {
415 allow(&caller, "start", &req.workflow)?;
416 let r = engine(&state).start(
417 &req.workflow,
418 req.version.as_deref(),
419 req.input,
420 req.execution_id,
421 )?;
422 let status = if r.created {
423 StatusCode::CREATED
424 } else {
425 StatusCode::OK
426 };
427 Ok((status, Json(serde_json::to_value(r).unwrap_or_default())).into_response())
428}
429
430#[derive(Deserialize)]
431struct ListQuery {
432 status: Option<String>,
433 workflow: Option<String>,
434 limit: Option<u32>,
435}
436
437async fn list_executions(
438 State(state): State<Shared>,
439 Extension(caller): Extension<Caller>,
440 Query(q): Query<ListQuery>,
441) -> ApiResult<Json<Value>> {
442 let limit = q.limit.unwrap_or(50).clamp(1, 1000);
443 let fetch = if matches!(caller, Caller::Client(_)) {
446 1000
447 } else {
448 limit
449 };
450 let mut list = engine(&state).list_executions(
451 q.status.as_deref().map(str::to_uppercase).as_deref(),
452 q.workflow.as_deref(),
453 fetch,
454 )?;
455 if let Caller::Client(c) = &caller {
456 list.retain(|e| c.can_read(&e.workflow));
457 list.truncate(limit as usize);
458 }
459 Ok(Json(json!({ "executions": list })))
460}
461
462async fn get_execution(
463 State(state): State<Shared>,
464 Extension(caller): Extension<Caller>,
465 Path(id): Path<String>,
466) -> ApiResult<Json<Value>> {
467 allow_execution(&state, &caller, "read", &id)?;
468 let s = engine(&state).execution(&id)?;
469 Ok(Json(serde_json::to_value(s).unwrap_or_default()))
470}
471
472async fn get_history(
473 State(state): State<Shared>,
474 Extension(caller): Extension<Caller>,
475 Path(id): Path<String>,
476) -> ApiResult<Json<Value>> {
477 allow_execution(&state, &caller, "read", &id)?;
478 let h = engine(&state).history(&id)?;
479 Ok(Json(json!({ "execution_id": id, "events": h })))
480}
481
482#[derive(Deserialize, Default)]
483struct CancelRequest {
484 #[serde(default)]
485 reason: Option<String>,
486}
487
488async fn cancel_execution(
489 State(state): State<Shared>,
490 Extension(caller): Extension<Caller>,
491 Path(id): Path<String>,
492 body: Option<Json<CancelRequest>>,
493) -> ApiResult<Json<Value>> {
494 allow_execution(&state, &caller, "cancel", &id)?;
495 let reason = body
496 .and_then(|b| b.0.reason)
497 .unwrap_or_else(|| "requested via API".into());
498 let s = engine(&state).cancel(&id, &reason)?;
499 Ok(Json(serde_json::to_value(s).unwrap_or_default()))
500}
501
502async fn send_signal(
505 State(state): State<Shared>,
506 Extension(caller): Extension<Caller>,
507 Path((id, name)): Path<(String, String)>,
508 body: Option<Json<Value>>,
509) -> ApiResult<Json<Value>> {
510 allow_execution(&state, &caller, "signal", &id)?;
511 let payload = body.map(|b| b.0).unwrap_or_else(|| json!({}));
512 let seq = engine(&state).signal(&id, &name, payload)?;
513 Ok(Json(json!({
514 "execution_id": id,
515 "signal": name,
516 "accepted": true,
517 "seq": seq,
518 })))
519}
520
521async fn run_query(
523 State(state): State<Shared>,
524 Extension(caller): Extension<Caller>,
525 Path((id, name)): Path<(String, String)>,
526) -> ApiResult<Json<Value>> {
527 allow_execution(&state, &caller, "query", &id)?;
528 let value = engine(&state).query(&id, &name)?;
529 Ok(Json(
530 json!({ "execution_id": id, "query": name, "value": value }),
531 ))
532}
533
534#[derive(Deserialize)]
535struct WaitQuery {
536 timeout_ms: Option<u64>,
537}
538
539async fn wait_execution(
541 State(state): State<Shared>,
542 Extension(caller): Extension<Caller>,
543 Path(id): Path<String>,
544 Query(q): Query<WaitQuery>,
545) -> ApiResult<Json<Value>> {
546 allow_execution(&state, &caller, "read", &id)?;
547 let deadline = tokio::time::Instant::now()
548 + Duration::from_millis(q.timeout_ms.unwrap_or(30_000).min(300_000));
549 loop {
550 let notified = state.changed.notified();
551 tokio::pin!(notified);
552 notified.as_mut().enable();
553 let s = engine(&state).execution(&id)?;
554 if s.status.is_terminal() || tokio::time::Instant::now() >= deadline {
555 return Ok(Json(serde_json::to_value(s).unwrap_or_default()));
556 }
557 let _ = tokio::time::timeout_at(deadline, notified).await;
558 }
559}
560
561async fn list_workers(State(state): State<Shared>) -> ApiResult<Json<Value>> {
562 Ok(Json(json!({ "workers": engine(&state).workers()? })))
563}
564
565async fn register_worker(
566 State(state): State<Shared>,
567 Extension(caller): Extension<Caller>,
568 Json(req): Json<RegisterWorker>,
569) -> ApiResult<Json<Value>> {
570 if let Some(id) = caller.identity() {
571 if req.worker_id != id.id {
572 return Err(forbidden(format!(
573 "this credential belongs to worker `{}`, not `{}`",
574 id.id, req.worker_id
575 )));
576 }
577 if let Some(rt) = &id.runtime {
578 if !rt.eq_ignore_ascii_case(&req.runtime) {
579 return Err(forbidden(format!(
580 "worker `{}` must announce runtime `{rt}`",
581 id.id
582 )));
583 }
584 }
585 let denied: Vec<&str> = req
586 .contracts
587 .iter()
588 .map(|c| c.task_name.as_str())
589 .filter(|t| !id.may_serve(t))
590 .collect();
591 if !denied.is_empty() {
592 return Err(forbidden(format!(
593 "worker `{}` may not serve: {} (allowed: {})",
594 id.id,
595 denied.join(", "),
596 id.tasks.join(", ")
597 )));
598 }
599 }
600 Ok(Json(
601 serde_json::to_value(engine(&state).register_worker(req)?).unwrap_or_default(),
602 ))
603}
604
605#[derive(Deserialize)]
606struct SessionRequest {
607 worker_session_id: String,
608 #[serde(default)]
609 wait_ms: Option<u64>,
610}
611
612async fn heartbeat(
613 State(state): State<Shared>,
614 Extension(caller): Extension<Caller>,
615 Json(req): Json<SessionRequest>,
616) -> ApiResult<Json<Value>> {
617 authorize_session(&state, &caller, &req.worker_session_id)?;
618 Ok(Json(
619 serde_json::to_value(engine(&state).heartbeat(&req.worker_session_id)?).unwrap_or_default(),
620 ))
621}
622
623async fn unregister(
624 State(state): State<Shared>,
625 Extension(caller): Extension<Caller>,
626 Json(req): Json<SessionRequest>,
627) -> ApiResult<Json<Value>> {
628 authorize_session(&state, &caller, &req.worker_session_id)?;
629 engine(&state).unregister_worker(&req.worker_session_id)?;
630 Ok(Json(json!({ "accepted": true })))
631}
632
633async fn poll(
635 State(state): State<Shared>,
636 Extension(caller): Extension<Caller>,
637 Json(req): Json<SessionRequest>,
638) -> ApiResult<Response> {
639 authorize_session(&state, &caller, &req.worker_session_id)?;
640 let wait = Duration::from_millis(req.wait_ms.unwrap_or(20_000).min(60_000));
641 let deadline = tokio::time::Instant::now() + wait;
642 loop {
643 let notified = state.changed.notified();
644 tokio::pin!(notified);
645 notified.as_mut().enable();
646 let task: Option<TaskAssignment> = crate::shared::poll(&state, &req.worker_session_id)?;
647 if let Some(t) = task {
648 return Ok(Json(serde_json::to_value(t).unwrap_or_default()).into_response());
649 }
650 if tokio::time::Instant::now() >= deadline {
651 return Ok(StatusCode::NO_CONTENT.into_response());
652 }
653 let _ = tokio::time::timeout_at(deadline, notified).await;
654 }
655}
656
657#[derive(Deserialize)]
658struct CompleteRequest {
659 token: String,
660 #[serde(default)]
661 output: Value,
662}
663
664async fn complete_task(
665 State(state): State<Shared>,
666 Extension(caller): Extension<Caller>,
667 Json(req): Json<CompleteRequest>,
668) -> ApiResult<Json<Value>> {
669 if req.token.is_empty() {
670 return Err(bad_request("token is required"));
671 }
672 authorize_token(&state, &caller, &req.token)?;
673 Ok(Json(
674 serde_json::to_value(engine(&state).complete(&req.token, req.output)?).unwrap_or_default(),
675 ))
676}
677
678#[derive(Deserialize)]
679struct FailRequest {
680 token: String,
681 error_code: String,
682 #[serde(default)]
683 error_message: String,
684 #[serde(default = "default_true")]
685 retryable: bool,
686 #[serde(default)]
687 details: Option<Value>,
688}
689
690fn default_true() -> bool {
691 true
692}
693
694async fn fail_task(
695 State(state): State<Shared>,
696 Extension(caller): Extension<Caller>,
697 Json(req): Json<FailRequest>,
698) -> ApiResult<Json<Value>> {
699 if req.error_code.is_empty() {
700 return Err(bad_request("error_code is required"));
701 }
702 authorize_token(&state, &caller, &req.token)?;
703 let error = TaskError {
704 code: req.error_code,
705 message: req.error_message,
706 retryable: req.retryable,
707 details: req.details,
708 };
709 Ok(Json(
710 serde_json::to_value(engine(&state).fail(&req.token, error)?).unwrap_or_default(),
711 ))
712}
713
714pub async fn run_ticker(state: Shared, interval: Duration) {
716 let mut ticker = tokio::time::interval(interval);
717 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
718 loop {
719 ticker.tick().await;
720 let jobs = match engine(&state).tick() {
721 Ok(j) => j,
722 Err(e) => {
723 tracing::error!(error = %e, "tick failed");
724 continue;
725 }
726 };
727 for job in jobs {
728 let st = state.clone();
729 tokio::spawn(async move {
730 let input = job.input.clone();
731 let timeout = job.timeout_ms;
732 let result = tokio::task::spawn_blocking(move || {
733 crate::http_adapter::execute(&input, timeout)
734 })
735 .await
736 .unwrap_or_else(|e| Err(TaskError::new("INTERNAL", e.to_string(), true)));
737 let mut e = engine(&st);
738 let r = match result {
739 Ok(out) => e.complete(&job.token, out),
740 Err(err) => e.fail(&job.token, err),
741 };
742 if let Err(err) = r {
743 tracing::error!(error = %err, "recording built-in task result failed");
744 }
745 });
746 }
747 }
748}
749
750pub struct TlsConfig {
752 pub cert: std::path::PathBuf,
753 pub key: std::path::PathBuf,
754 pub client_ca: Option<std::path::PathBuf>,
758}
759
760fn io_err(msg: String) -> std::io::Error {
761 std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)
762}
763
764fn read_pem_certs(
765 path: &std::path::Path,
766) -> std::io::Result<Vec<rustls::pki_types::CertificateDer<'static>>> {
767 let pem =
768 std::fs::read(path).map_err(|e| io_err(format!("cannot read {}: {e}", path.display())))?;
769 let certs = rustls_pemfile::certs(&mut pem.as_slice())
770 .collect::<Result<Vec<_>, _>>()
771 .map_err(|e| io_err(format!("{}: {e}", path.display())))?;
772 if certs.is_empty() {
773 return Err(io_err(format!(
774 "{} contains no certificates",
775 path.display()
776 )));
777 }
778 Ok(certs)
779}
780
781fn rustls_config(tls: &TlsConfig) -> std::io::Result<rustls::ServerConfig> {
783 let chain = read_pem_certs(&tls.cert)?;
784 let key_pem = std::fs::read(&tls.key)
785 .map_err(|e| io_err(format!("cannot read {}: {e}", tls.key.display())))?;
786 let key = rustls_pemfile::private_key(&mut key_pem.as_slice())
787 .map_err(|e| io_err(format!("{}: {e}", tls.key.display())))?
788 .ok_or_else(|| io_err(format!("{} contains no private key", tls.key.display())))?;
789 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
790 let builder = rustls::ServerConfig::builder_with_provider(provider.clone())
791 .with_safe_default_protocol_versions()
792 .map_err(|e| io_err(e.to_string()))?;
793 let builder = match &tls.client_ca {
794 Some(ca) => {
795 let mut roots = rustls::RootCertStore::empty();
796 for c in read_pem_certs(ca)? {
797 roots
798 .add(c)
799 .map_err(|e| io_err(format!("{}: {e}", ca.display())))?;
800 }
801 let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
802 Arc::new(roots),
803 provider,
804 )
805 .allow_unauthenticated()
806 .build()
807 .map_err(|e| io_err(format!("client CA: {e}")))?;
808 builder.with_client_cert_verifier(verifier)
809 }
810 None => builder.with_no_client_auth(),
811 };
812 let mut config = builder
813 .with_single_cert(chain, key)
814 .map_err(|e| io_err(format!("TLS certificate/key: {e}")))?;
815 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
816 Ok(config)
817}
818
819pub struct ServerConfig {
820 pub addr: SocketAddr,
821 pub auth_token: Option<String>,
822 pub worker_token: Option<String>,
823 pub identities: Identities,
824 pub tls: Option<TlsConfig>,
825 pub grpc_addr: Option<SocketAddr>,
827 pub listener: Option<std::net::TcpListener>,
830 pub grpc_listener: Option<std::net::TcpListener>,
831 pub body_limit: usize,
832 pub tick_interval: Duration,
833}
834
835pub async fn serve(
836 engine: Engine,
837 config: ServerConfig,
838 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
839) -> std::io::Result<()> {
840 serve_node(engine, config, None, shutdown).await
841}
842
843pub async fn serve_node(
847 engine: Engine,
848 config: ServerConfig,
849 cluster: Option<Arc<crate::cluster::Node>>,
850 shutdown: impl std::future::Future<Output = ()> + Send + 'static,
851) -> std::io::Result<()> {
852 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
855 let changed = engine.changed.clone();
856 let shared = engine.shared_context();
858 let state = Arc::new(AppState {
859 engine: Mutex::new(engine),
860 changed,
861 auth_token: config.auth_token,
862 worker_token: config.worker_token,
863 identities: config.identities,
864 });
865 let app = router(state.clone(), config.body_limit);
866 let app = match &shared {
867 Some(ctx) => crate::shared::layer(app, ctx.clone(), state.clone()),
868 None => crate::cluster::layer(app, cluster.clone(), state.clone()),
869 };
870 if let Some(node) = &cluster {
871 crate::cluster::start(node.clone(), state.clone())?;
872 }
873 if let Some(ctx) = &shared {
874 crate::shared::start(ctx.clone(), state.clone())?;
875 }
876 let ticker = tokio::spawn(run_ticker(state.clone(), config.tick_interval));
877 let (stop_tx, stop_rx) = tokio::sync::watch::channel(false);
879 tokio::spawn(async move {
880 shutdown.await;
881 let _ = stop_tx.send(true);
882 });
883 let stopped = |mut rx: tokio::sync::watch::Receiver<bool>| async move {
884 let _ = rx.wait_for(|v| *v).await;
885 };
886 let bind = |addr: SocketAddr| {
887 std::net::TcpListener::bind(addr)
888 .map_err(|e| std::io::Error::new(e.kind(), format!("cannot listen on {addr}: {e}")))
889 };
890 let grpc_listener = match (config.grpc_listener, config.grpc_addr) {
891 (Some(l), _) => Some(l),
892 (None, Some(addr)) => Some(bind(addr)?),
893 (None, None) => None,
894 };
895 let http_listener = match config.listener {
896 Some(l) => l,
897 None => bind(config.addr)?,
898 };
899 http_listener.set_nonblocking(true)?;
900 let http_addr = http_listener.local_addr()?;
901 let grpc = match grpc_listener {
902 Some(std_listener) => {
903 std_listener.set_nonblocking(true)?;
904 let addr = std_listener.local_addr()?;
905 let listener = tokio::net::TcpListener::from_std(std_listener)?;
906 let incoming =
907 tonic::transport::server::TcpIncoming::from_listener(listener, true, None)
908 .map_err(|e| io_err(format!("gRPC listener: {e}")))?;
909 let mut builder = tonic::transport::Server::builder();
910 if let Some(tls) = &config.tls {
911 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
912 let read = |p: &std::path::Path| {
913 std::fs::read(p)
914 .map_err(|e| io_err(format!("cannot read {}: {e}", p.display())))
915 };
916 let mut t = tonic::transport::ServerTlsConfig::new().identity(
917 tonic::transport::Identity::from_pem(read(&tls.cert)?, read(&tls.key)?),
918 );
919 if let Some(ca) = &tls.client_ca {
920 t = t
921 .client_ca_root(tonic::transport::Certificate::from_pem(read(ca)?))
922 .client_auth_optional(true);
923 }
924 builder = builder
925 .tls_config(t)
926 .map_err(|e| io_err(format!("gRPC TLS: {e}")))?;
927 }
928 let service = crate::grpc::GrpcWorkers::new(state.clone());
929 let rx = stop_rx.clone();
930 tracing::info!(addr = %addr, tls = config.tls.is_some(), "gRPC worker protocol listening");
931 let router = match &cluster {
932 Some(node) => {
933 builder.add_service(tonic::service::interceptor::InterceptedService::new(
934 service,
935 crate::cluster::grpc_gate(node.clone()),
936 ))
937 }
938 None => builder.add_service(service),
939 };
940 Some(tokio::spawn(async move {
941 if let Err(e) = router
942 .serve_with_incoming_shutdown(incoming, stopped(rx))
943 .await
944 {
945 tracing::error!(error = %e, "gRPC server failed");
946 }
947 }))
948 }
949 None => None,
950 };
951 let shutdown = stopped(stop_rx);
952 let result = match config.tls {
953 None => {
954 let listener = tokio::net::TcpListener::from_std(http_listener)?;
955 tracing::info!(addr = %http_addr, tls = false, "ORBIT runtime listening");
956 axum::serve(listener, app)
957 .with_graceful_shutdown(shutdown)
958 .await
959 }
960 Some(tls) => {
961 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
964 let rustls =
965 axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(rustls_config(&tls)?));
966 let handle = axum_server::Handle::new();
967 let h = handle.clone();
968 tokio::spawn(async move {
969 shutdown.await;
970 h.graceful_shutdown(Some(Duration::from_secs(10)));
971 });
972 tracing::info!(addr = %http_addr, tls = true, mtls = tls.client_ca.is_some(), "ORBIT runtime listening");
973 axum_server::from_tcp_rustls(http_listener, rustls)?
974 .handle(handle)
975 .serve(app.into_make_service())
976 .await
977 }
978 };
979 ticker.abort();
980 if let Some(ctx) = &shared {
981 ctx.stop();
982 }
983 if let Some(g) = grpc {
984 let _ = g.await;
985 }
986 result
987}