Skip to main content

ordinary_api/server/
mod.rs

1// Copyright (C) 2026 Ordinary Labs, LLC.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4
5mod ops;
6
7use axum::extract::ConnectInfo;
8use axum::http::{HeaderValue, Uri};
9use axum::http::{Request, header};
10use axum::response::Redirect;
11use hashbrown::{HashMap, HashSet};
12use hyper::HeaderMap;
13use hyper_util::rt::TokioIo;
14use ordinary_app::server::OrdinaryAppServer;
15#[cfg(feature = "server")]
16use ordinary_config::OrdinaryApiLimits;
17use ordinary_monitor::Manager;
18use ordinary_storage::Storage;
19use ordinary_storage::saferlmdb::{self, EnvBuilder, Environment};
20use parking_lot::RwLock;
21use rand_chacha::rand_core::Rng;
22use rand_chacha::rand_core::SeedableRng;
23use sha2::{Digest, Sha256};
24use std::fs::File;
25use std::io::Write;
26use std::process;
27use tokio::net::TcpListener;
28use tokio::net::TcpStream;
29use tokio_rustls::LazyConfigAcceptor;
30use tokio_rustls::StartHandshake;
31
32use axum::Router;
33use axum::http::HeaderName;
34use axum::response::Response;
35use axum::routing::{delete, get, patch, post, put};
36
37use axum::http::StatusCode;
38use tower_http::classify::ServerErrorsFailureClass;
39use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
40use uuid::Uuid;
41
42use std::net::SocketAddr;
43use std::path::Path;
44use std::path::PathBuf;
45use std::sync::Arc;
46use std::time::Duration;
47
48use parking_lot::Mutex;
49
50use rustls_acme::{AcmeConfig, caches::DirCache};
51use tokio_rustls::{TlsAcceptor, rustls::ServerConfig};
52use tower::ServiceBuilder;
53use tower_http::compression::CompressionLayer;
54use tower_http::decompression::RequestDecompressionLayer;
55use tower_http::trace::TraceLayer;
56use tracing::Span;
57
58use ordinary_auth::{Auth, AuthClient, OsRng};
59
60use std::fs;
61
62use crate::{api_account_claims, api_invite_claims};
63use anyhow::bail;
64use axum::middleware::Next;
65use getrandom::SysRng;
66use hickory_client::client::Client;
67use hickory_client::proto::runtime::TokioRuntimeProvider;
68use hickory_client::proto::udp::UdpClientStream;
69use ordinary_config::{
70    AccessTokenConfig, AuthConfig, HmacTokenAlgorithm, InviteConfig, InviteMode, MfaConfig,
71    OrdinaryApiConfig, OrdinaryConfig, PasswordConfig, RedactedHashAlg, RefreshTokenConfig,
72    SignedTokenAlgorithm, TotpConfig,
73};
74use ordinary_utils::middleware::modify_etag_for_encoding;
75use ordinary_utils::{
76    GMT_FORMAT, HeadersDebug, LatencyDisplay, REQUEST_ID_HEADER, WrappedRedactedHashingAlg,
77    get_bearer_token_as_bytes, get_host, redirect_service, response_for_panic,
78    rustls_server_config, shutdown_signal,
79};
80use time::UtcDateTime;
81use tokio::sync::watch;
82use tokio::sync::watch::{Receiver, Sender};
83use tokio::time::timeout;
84use tokio_rustls::rustls::crypto::ring;
85use tokio_rustls::rustls::server::Acceptor;
86use tower_http::catch_panic::CatchPanicLayer;
87use tower_http::set_header::SetResponseHeaderLayer;
88use tower_http::timeout::TimeoutLayer;
89use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme};
90use utoipa::{Modify, OpenApi};
91use utoipa_swagger_ui::{Config, SwaggerUi};
92use x25519_dalek::{PublicKey, StaticSecret};
93
94pub const ROOT: &str = "root";
95pub const ADMIN: &str = "admin";
96pub const APPLICATION: &str = "application";
97pub const AUTHENTICATION: &str = "authentication";
98
99#[derive(OpenApi)]
100#[openapi(
101    info(
102        title = "Ordinary API Server",
103        description = "
104## ordinary-api
105
106[![docs.rs](https://img.shields.io/docsrs/ordinary-api/0.6.0-pre.5)](https://docs.rs/ordinary-api/0.6.0-pre.5)
107[![dependency status](https://img.shields.io/deps-rs/ordinary-api/0.6.0-pre.5)](https://deps.rs/crate/ordinary-api/0.6.0-pre.5)
108
109### Access Token
110
111Use the [Ordinary CLI](https://crates.io/crates/ordinary) to get an access token.
112
113```sh
114ordinary accounts access get --min 15
115```
116",
117        contact(),
118    ),
119    modifiers(&Security),
120    nest(
121        (path = "/v1", api = OrdinaryApiOpenApi),
122    ),
123    tags(
124        (name = APPLICATION, description = "Application API"),
125        (name = ADMIN, description = "Admin API"),
126        (name = ROOT, description = "Root API"),
127        (name = AUTHENTICATION, description = "Authentication API"),
128    )
129)]
130struct ApiDoc;
131
132struct Security;
133
134impl Modify for Security {
135    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
136        if let Some(components) = openapi.components.as_mut() {
137            components.add_security_scheme(
138                "invite",
139                SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
140            );
141
142            components.add_security_scheme(
143                "refresh",
144                SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
145            );
146
147            components.add_security_scheme(
148                "access",
149                SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
150            );
151        }
152    }
153}
154
155#[derive(OpenApi)]
156#[openapi(paths(
157    ops::accounts::invite,
158    ops::accounts::list,
159    ops::accounts::delete,
160    ops::accounts::registration_start,
161    ops::accounts::registration_finish,
162    ops::accounts::login_start,
163    ops::accounts::login_finish,
164    ops::accounts::access_get,
165    ops::accounts::password_reset_login_start,
166    ops::accounts::password_reset_login_finish,
167    ops::accounts::password_reset_registration_start,
168    ops::accounts::password_reset_registration_finish,
169    ops::accounts::password_forgot_start,
170    ops::accounts::password_forgot_finish,
171    ops::accounts::mfa_reset_totp_start,
172    ops::accounts::mfa_reset_totp_finish,
173    ops::accounts::mfa_lost_totp_start,
174    ops::accounts::mfa_lost_totp_finish,
175    ops::accounts::recovery_reset_codes_start,
176    ops::accounts::recovery_reset_codes_finish,
177    ops::accounts::account_delete_start,
178    ops::accounts::account_delete_finish,
179    ops::root::logs,
180    ops::app::logs,
181    ops::app::deploy,
182    ops::app::patch,
183    ops::app::kill,
184    ops::app::restart,
185    ops::templates::upload,
186    ops::secrets::store,
187    ops::secrets::public_key,
188    ops::content::update,
189    ops::assets::write,
190    ops::models::items_list,
191    ops::actions::install,
192))]
193pub struct OrdinaryApiOpenApi;
194
195/// "admin" 0 | "read" 1 | "write" 2 | "update" 3 | "upload" 4 | "install" 5 | "deploy" 6 | "bridge" 7 | "kill" 8 | "erase" 9
196pub(crate) fn check_ordinary_auth(
197    state: &OrdinaryApiServerState,
198    headers: &HeaderMap,
199    permission: u8,
200    domain: &str,
201) -> Result<String, StatusCode> {
202    if let Ok(token) = get_bearer_token_as_bytes(headers) {
203        match state.auth.verify_access_token(token.as_ref()) {
204            Ok((account, claims)) => {
205                let span = tracing::info_span!("permission", account);
206
207                span.in_scope(|| {
208                    if account == "root" {
209                        return Ok(account.to_string());
210                    }
211
212                    if domain == "root" {
213                        tracing::error!("root-only operation disallowed");
214                        return Err(StatusCode::UNAUTHORIZED);
215                    }
216
217                    let check_domain = claims.idx(1).as_str();
218
219                    if check_domain != domain {
220                        tracing::error!(domain, "token domain '{check_domain}' does not match");
221                        return Err(StatusCode::UNAUTHORIZED);
222                    }
223
224                    let permissions = claims.idx(2).as_vector();
225
226                    for claim_permission in &permissions {
227                        if claim_permission.as_u8() == 0 || claim_permission.as_u8() == permission {
228                            tracing::info!(
229                                role = match claim_permission.as_u8() {
230                                    0 => "admin",
231                                    1 => "read",
232                                    2 => "write",
233                                    3 => "update",
234                                    4 => "upload",
235                                    5 => "install",
236                                    6 => "deploy",
237                                    7 => "bridge",
238                                    8 => "kill",
239                                    9 => "erase",
240                                    _ => "invalid",
241                                },
242                                "granted"
243                            );
244
245                            return Ok(account.to_string());
246                        }
247                    }
248
249                    tracing::error!(
250                        permission = match permission {
251                            0 => "admin",
252                            1 => "read",
253                            2 => "write",
254                            3 => "update",
255                            4 => "upload",
256                            5 => "install",
257                            6 => "deploy",
258                            7 => "bridge",
259                            8 => "kill",
260                            9 => "erase",
261                            _ => "invalid",
262                        },
263                        "not in token permissions"
264                    );
265                    Err(StatusCode::UNAUTHORIZED)
266                })
267            }
268            Err(err) => {
269                tracing::error!(%err, "access denied");
270                Err(StatusCode::UNAUTHORIZED)
271            }
272        }
273    } else {
274        tracing::error!("invalid token");
275        Err(StatusCode::UNAUTHORIZED)
276    }
277}
278
279pub struct WrappedOrdinaryAppServer {
280    port: u16,
281    app: Arc<OrdinaryAppServer>,
282    kill_tx: tokio::sync::broadcast::Sender<()>,
283    stream_tx: tokio::sync::mpsc::UnboundedSender<(StartHandshake<TcpStream>, SocketAddr, Span)>,
284    secrets_public_keypair: (StaticSecret, PublicKey),
285}
286
287type Dbs = HashMap<String, (Arc<Environment>, Arc<Auth>, Arc<Storage>)>;
288
289#[allow(clippy::struct_excessive_bools)]
290pub struct OrdinaryApiServerState {
291    pub domain: String,
292    pub app_domains: Arc<Vec<String>>,
293    pub secure: bool,
294    pub secure_cookies: bool,
295    pub log_headers: bool,
296    pub log_ips: bool,
297    pub log_size: bool,
298    pub auth: Arc<Auth>,
299    pub apps: Arc<RwLock<HashMap<String, Arc<WrappedOrdinaryAppServer>>>>,
300    pub apps_dir: PathBuf,
301    pub dbs: Arc<Mutex<Dbs>>,
302    pub env_name: String,
303    pub provision_mode: Option<ProvisionMode>,
304    pub dedicated_ports: bool,
305    pub server_span: Span,
306    pub log_manager: Arc<Option<Manager>>,
307    pub log_index: bool,
308    pub limits: String,
309    pub config: OrdinaryApiConfig,
310
311    pub signal_tx: Arc<RwLock<Option<Sender<()>>>>,
312    pub close_rx: Arc<RwLock<Option<Receiver<()>>>>,
313
314    pub dns_client: Arc<tokio::sync::Mutex<Client>>,
315
316    pub privileged_domains: HashSet<String>,
317}
318
319#[derive(PartialEq, Clone)]
320pub enum ProvisionMode {
321    Localhost,
322    Staging,
323    Production,
324}
325
326pub enum SecurityMode<T: AsRef<Path>> {
327    Insecure,
328    Secure(T, ProvisionMode),
329}
330
331pub struct OrdinaryApiServer {
332    auth: Arc<Auth>,
333    _env: Arc<Environment>,
334    config: OrdinaryApiConfig,
335    apps: Arc<RwLock<HashMap<String, Arc<WrappedOrdinaryAppServer>>>>,
336    apps_dir: PathBuf,
337    log_manager: Arc<Option<Manager>>,
338}
339
340#[cfg(feature = "server")]
341impl OrdinaryApiServer {
342    #[allow(clippy::similar_names)]
343    pub fn init(
344        env_name: &str,
345        domain: &str,
346        password: &str,
347        base_dir_path: impl AsRef<Path>,
348        storage_size: usize,
349        log_manager: Arc<Option<Manager>>,
350    ) -> anyhow::Result<(String, Vec<u8>)> {
351        let span = tracing::info_span!("init", env = %env_name, pid = process::id());
352
353        span.in_scope(|| {
354            let config = OrdinaryApiConfig {
355                // todo: pass in as a flag
356                public_dns_ip: [8, 8, 8, 8],
357                env_name: env_name.to_string(),
358                limits: OrdinaryApiLimits::default(),
359            };
360            let config_file = serde_json::to_string_pretty(&config)?;
361
362            let environment_dir_path = base_dir_path.as_ref().join("environments").join(env_name);
363            let data_path = environment_dir_path.join("data");
364
365            if data_path.exists() {
366                bail!("environment already initialized");
367            }
368
369            fs::write(environment_dir_path.join("ordinaryd.json"), config_file)?;
370
371            let api_server =
372                OrdinaryApiServer::new(env_name, domain, base_dir_path, storage_size, log_manager)?;
373
374            let mut input = domain.as_bytes().to_vec();
375            input.extend_from_slice(b"root");
376
377            let mut password_input = input.clone();
378            password_input.extend_from_slice(password.as_bytes());
379
380            let mut hasher = Sha256::new();
381            hasher.update(&password_input);
382            let password = hasher.finalize().to_vec();
383
384            let invite_token = api_server.auth.api_invite_get(domain, "root", None)?;
385
386            let (state, reg_start_req) = AuthClient::registration_start_req(b"root", &password)?;
387
388            let checked_claims = api_server.auth.invite_check(&invite_token)?;
389            let reg_start_res = api_server.auth.registration_start(
390                reg_start_req,
391                None,
392                None,
393                Some(checked_claims),
394            )?;
395
396            let (private_key, reg_finish_req) =
397                AuthClient::registration_finish_req(b"root", &password, &state, &reg_start_res)?;
398            let (reg_finish_res, ..) = api_server.auth.registration_finish(reg_finish_req, None)?;
399
400            let (totp, _recovery_codes) = AuthClient::decrypt_totp_mfa(
401                &reg_finish_res,
402                private_key,
403                env_name.to_string(),
404                "root".into(),
405            )?;
406
407            Ok((totp.get_url(), totp.secret))
408        })
409    }
410
411    pub fn new(
412        env_name: &str,
413        domain: &str,
414        base_dir_path: impl AsRef<Path>,
415        storage_size: usize,
416        log_manager: Arc<Option<Manager>>,
417    ) -> anyhow::Result<OrdinaryApiServer> {
418        use ordinary_config::{HmacTokenConfig, SignedTokenConfig};
419
420        let span = tracing::info_span!("setup", env = %env_name, pid = process::id());
421
422        span.in_scope(|| {
423            let environment_dir_path = base_dir_path.as_ref().join("environments").join(env_name);
424
425            let config: OrdinaryApiConfig = serde_json::from_str(&fs::read_to_string(
426                environment_dir_path.join("ordinaryd.json"),
427            )?)?;
428
429            let data_path = environment_dir_path.join("data");
430
431            fs::create_dir_all(&data_path)?;
432
433            let ps = page_size::get();
434
435            // round up to full OS page
436            let remainder = storage_size % ps;
437            let mapsize = (storage_size - remainder) + ps;
438
439            tracing::info!(mapsize = %bytesize::ByteSize(mapsize as u64).display().si_short());
440
441            let env = Arc::new(unsafe {
442                let mut env_builder = EnvBuilder::new()?;
443                env_builder.set_maxreaders(126)?;
444                env_builder.set_mapsize(mapsize)?;
445                env_builder.set_maxdbs(13)?;
446                env_builder.open(
447                    match data_path.to_str() {
448                        Some(v) => v,
449                        None => bail!("data_path not a str"),
450                    },
451                    &saferlmdb::open::Flags::empty(),
452                    0o600,
453                )?
454            });
455
456            let keys_dir = environment_dir_path.join("keys");
457            fs::create_dir_all(&keys_dir)?;
458
459            let auth_key_path = keys_dir.join("auth");
460
461            let auth_key: [u8; 32] = if auth_key_path.exists() && auth_key_path.is_file() {
462                let auth_key = fs::read(&auth_key_path)?;
463                let auth_key: [u8; 32] = auth_key[..].try_into()?;
464                auth_key
465            } else {
466                let mut auth_key = [0u8; 32];
467                let mut rng = rand_chacha::ChaCha20Rng::try_from_rng(&mut SysRng)?;
468
469                rng.fill_bytes(&mut auth_key[..]);
470
471                let mut auth_key_file = File::create(auth_key_path)?;
472                auth_key_file.write_all(&auth_key)?;
473
474                auth_key
475            };
476
477            let auth = Arc::new(Auth::new(
478                domain.into(),
479                Some(AuthConfig {
480                    password: PasswordConfig {
481                        protocol: ordinary_config::PasswordProtocol::Opaque,
482                    },
483                    mfa: MfaConfig {
484                        totp: TotpConfig {
485                            template: None,
486                            algorithm: ordinary_config::TotpAlgorithm::Sha1,
487                        },
488                    },
489                    hmac_token: HmacTokenConfig {
490                        algorithm: HmacTokenAlgorithm::HmacBlake2b256,
491                        rotation: 60 * 60 * 24 * 7,
492                    },
493                    signed_token: SignedTokenConfig {
494                        algorithm: SignedTokenAlgorithm::DsaEd25519,
495                        rotation: 60 * 60 * 24 * 30,
496                    },
497                    refresh_token: RefreshTokenConfig {
498                        lifetime: 60 * 60 * 24 * 7,
499                    },
500                    access_token: AccessTokenConfig {
501                        lifetime: 60 * 60 * 24,
502                        claims: api_account_claims(),
503                    },
504                    client_hash: ordinary_config::ClientPasswordHash::Sha256,
505                    cookies_enabled: true,
506                    invite: Some(InviteConfig {
507                        // todo: make `InviteMode` configurable
508                        mode: InviteMode::Root,
509                        lifetime: 60 * 60 * 24,
510                        clean_interval: (30, 90),
511                        claims: Some(api_invite_claims()),
512                    }),
513                }),
514                auth_key,
515                env.clone(),
516            )?);
517
518            let apps_dir = environment_dir_path.join("apps");
519            fs::create_dir_all(&apps_dir)?;
520
521            Ok(OrdinaryApiServer {
522                config,
523                auth,
524                _env: env,
525                apps: Arc::new(RwLock::new(HashMap::new())),
526                apps_dir,
527                log_manager,
528            })
529        })
530    }
531
532    #[allow(
533        clippy::too_many_arguments,
534        clippy::fn_params_excessive_bools,
535        clippy::too_many_lines
536    )]
537    pub async fn start<P, F>(
538        &self,
539        server_span: Span,
540        mode: SecurityMode<P>,
541        listener: TcpListener,
542        secure_cookies: bool,
543        log_headers: bool,
544        log_ips: bool,
545        log_size: bool,
546        api_domain: String,
547        api_contacts: &[String],
548        privileged_domains: &Option<Vec<String>>,
549        app_domains: &[String],
550        redirect_listener: Option<TcpListener>,
551        dedicated_ports: bool,
552        log_index: bool,
553        redacted_hash: Option<RedactedHashAlg>,
554        swagger: bool,
555        signal: fn() -> F,
556    ) -> anyhow::Result<()>
557    where
558        P: AsRef<Path>,
559        F: Future<Output = ()> + Send + 'static,
560    {
561        use axum::extract::State;
562
563        let start_span = tracing::info_span!(
564            "start",
565            env = &self.config.env_name,
566            pid = process::id(),
567            domain = api_domain
568        );
569
570        let address = SocketAddr::from((self.config.public_dns_ip, 53));
571        let conn = UdpClientStream::builder(address, TokioRuntimeProvider::default()).build();
572
573        let (client, bg) = Client::connect(conn).await?;
574        tokio::spawn(bg);
575
576        let server_span_clone = server_span.clone();
577        let server_span_clone2 = server_span.clone();
578        let server_span_clone3 = server_span.clone();
579
580        let api_domain_clone = api_domain.clone();
581
582        let limits = serde_json::to_string(&self.config.limits)?;
583
584        let state = start_span.in_scope(|| {
585            Arc::new(OrdinaryApiServerState {
586                domain: api_domain.clone(),
587                app_domains: Arc::new(app_domains.to_vec()),
588                secure: !matches!(mode, SecurityMode::Insecure),
589                secure_cookies,
590                log_headers,
591                log_ips,
592                log_size,
593                auth: self.auth.clone(),
594                apps: self.apps.clone(),
595                apps_dir: self.apps_dir.clone(),
596                dbs: Arc::new(Mutex::new(HashMap::new())),
597                env_name: self.config.env_name.clone(),
598                provision_mode: match &mode {
599                    SecurityMode::Insecure => None,
600                    SecurityMode::Secure(_, provision) => Some(provision.clone()),
601                },
602                dedicated_ports,
603                server_span: server_span.clone(),
604                log_manager: self.log_manager.clone(),
605                log_index,
606                limits,
607                config: self.config.clone(),
608
609                signal_tx: Arc::new(RwLock::new(None)),
610                close_rx: Arc::new(RwLock::new(None)),
611
612                dns_client: Arc::new(tokio::sync::Mutex::new(client)),
613
614                privileged_domains: if let Some(pd) = privileged_domains
615                    && !pd.is_empty()
616                {
617                    pd.iter().cloned().collect::<HashSet<_>>()
618                } else {
619                    HashSet::new()
620                },
621            })
622        });
623
624        let request_id = HeaderName::from_static(REQUEST_ID_HEADER);
625
626        let redacted_hash = if let Some(redacted_hash) = redacted_hash {
627            Arc::new(Some(WrappedRedactedHashingAlg(redacted_hash)))
628        } else {
629            Arc::new(None)
630        };
631
632        let redacted_hash_clone = redacted_hash.clone();
633        let redacted_hash_clone1 = redacted_hash.clone();
634
635        let last_modified = UtcDateTime::now();
636
637        let last_modified_string = last_modified.format(&GMT_FORMAT)?;
638        let last_modified_header = HeaderValue::from_str(last_modified_string.as_str())?;
639
640        let ordinaryd_resource_layers = ServiceBuilder::new()
641            .layer(axum::middleware::from_fn(
642                move |req_headers: HeaderMap, request: axum::extract::Request, next: Next| {
643                    ordinary_utils::middleware::http_cache_middleware(
644                        last_modified,
645                        req_headers,
646                        request,
647                        next,
648                    )
649                },
650            ))
651            .layer(SetResponseHeaderLayer::if_not_present(
652                header::LAST_MODIFIED,
653                last_modified_header.clone(),
654            ))
655            .layer(SetResponseHeaderLayer::if_not_present(
656                header::EXPIRES,
657                |_: &Response<_>| {
658                    let future = UtcDateTime::now() + time::Duration::minutes(10);
659
660                    if let Ok(formatted) = future.format(&GMT_FORMAT)
661                        && let Ok(expires) = HeaderValue::from_str(formatted.as_str())
662                    {
663                        return Some(expires);
664                    }
665
666                    None
667                },
668            ))
669            .layer(SetResponseHeaderLayer::if_not_present(
670                header::VARY,
671                HeaderValue::from_static(header::ACCEPT_ENCODING.as_str()),
672            ));
673
674        let mut service = Router::new();
675
676        if swagger {
677            // todo: rapidoc
678            // todo: scalar
679            // todo: redoc
680
681            service = service
682                .merge(
683                    SwaggerUi::new("/.ordinary/swagger").config(Config::from("/.ordinary/openapi")),
684                )
685                .route_layer(ordinaryd_resource_layers.clone().layer(
686                    SetResponseHeaderLayer::if_not_present(
687                        header::CONTENT_SECURITY_POLICY,
688                        HeaderValue::from_static(
689                            "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src https://img.shields.io",
690                        ),
691                    ),
692                ));
693        }
694
695        // todo: || openapi
696        if swagger {
697            let api = ApiDoc::openapi().to_json()?;
698
699            service = service.route(
700                "/.ordinary/openapi",
701                get(|| async move {
702                    (
703                        StatusCode::OK,
704                        [(header::CONTENT_TYPE, "application/json")],
705                        api,
706                    )
707                })
708                .route_layer(ordinaryd_resource_layers.clone()),
709            );
710        }
711
712        service = service.route(
713            "/.ordinary/limits",
714            get(
715                |State(state): State<Arc<OrdinaryApiServerState>>| async move {
716                    (
717                        StatusCode::OK,
718                        [(header::CONTENT_TYPE, "application/json")],
719                        state.limits.clone(),
720                    )
721                },
722            )
723            .route_layer(ordinaryd_resource_layers.clone()),
724        );
725
726        let mut service = service
727            .route("/healthz", get(|| async { StatusCode::OK }))
728            // auth ops
729            .route(
730                "/v1/accounts/registration/start",
731                post(ops::accounts::registration_start),
732            )
733            .route(
734                "/v1/accounts/registration/finish",
735                post(ops::accounts::registration_finish),
736            )
737            .route("/v1/accounts/login/start", post(ops::accounts::login_start))
738            .route(
739                "/v1/accounts/login/finish",
740                post(ops::accounts::login_finish),
741            )
742            .route("/v1/accounts/access", get(ops::accounts::access_get))
743            .route("/v1/accounts/logout", put(ops::accounts::logout))
744            // reset password
745            .route(
746                "/v1/accounts/password/reset/login/start",
747                post(ops::accounts::password_reset_login_start),
748            )
749            .route(
750                "/v1/accounts/password/reset/login/finish",
751                post(ops::accounts::password_reset_login_finish),
752            )
753            .route(
754                "/v1/accounts/password/reset/registration/start",
755                post(ops::accounts::password_reset_registration_start),
756            )
757            .route(
758                "/v1/accounts/password/reset/registration/finish",
759                post(ops::accounts::password_reset_registration_finish),
760            )
761            // forgot password
762            .route(
763                "/v1/accounts/password/forgot/start",
764                post(ops::accounts::password_forgot_start),
765            )
766            .route(
767                "/v1/accounts/password/forgot/finish",
768                post(ops::accounts::password_forgot_finish),
769            )
770            // reset totp mfa
771            .route(
772                "/v1/accounts/mfa/totp/reset/start",
773                post(ops::accounts::mfa_reset_totp_start),
774            )
775            .route(
776                "/v1/accounts/mfa/totp/reset/finish",
777                post(ops::accounts::mfa_reset_totp_finish),
778            )
779            // lost totp mfa
780            .route(
781                "/v1/accounts/mfa/totp/lost/start",
782                post(ops::accounts::mfa_lost_totp_start),
783            )
784            .route(
785                "/v1/accounts/mfa/totp/lost/finish",
786                post(ops::accounts::mfa_lost_totp_finish),
787            )
788            // delete account
789            .route(
790                "/v1/accounts/delete/start",
791                post(ops::accounts::account_delete_start),
792            )
793            .route(
794                "/v1/accounts/delete/finish",
795                post(ops::accounts::account_delete_finish),
796            )
797            // reset recovery codes
798            .route(
799                "/v1/accounts/recovery-codes/reset/start",
800                post(ops::accounts::recovery_reset_codes_start),
801            )
802            .route(
803                "/v1/accounts/recovery-codes/reset/finish",
804                post(ops::accounts::recovery_reset_codes_finish),
805            )
806            // root ops
807            .route("/v1/apps", get(ops::app::list))
808            .route("/v1/logs", get(ops::root::logs))
809            // admin ops
810            .route("/v1/accounts/invite", get(ops::accounts::invite))
811            .route("/v1/accounts", get(ops::accounts::list))
812            .route("/v1/accounts", delete(ops::accounts::delete))
813            // app account ops
814            .route("/v1/app/accounts/invite", get(ops::app::invite))
815            .route("/v1/app/accounts", get(ops::app::accounts_list))
816            // app ops
817            .route("/v1/app/deploy", post(ops::app::deploy))
818            .route("/v1/app/patch", patch(ops::app::patch))
819            .route("/v1/app/migrate", put(ops::app::migrate))
820            .route("/v1/app/bridge", put(ops::app::bridge))
821            .route("/v1/app/kill", put(ops::app::kill))
822            .route("/v1/app/restart", post(ops::app::restart))
823            .route("/v1/app/erase", put(ops::app::erase))
824            .route("/v1/app/logs", get(ops::app::logs))
825            // template ops
826            .route("/v1/templates", put(ops::templates::upload))
827            // content ops
828            .route("/v1/content", put(ops::content::update))
829            // secret ops
830            .route("/v1/secrets/public-key", get(ops::secrets::public_key))
831            .route("/v1/secrets", put(ops::secrets::store))
832            // assets ops
833            .route("/v1/assets", put(ops::assets::write))
834            // action ops
835            .route("/v1/actions", put(ops::actions::install))
836            // models ops
837            .route("/v1/models/items", get(ops::models::items_list))
838            .fallback(|| async { StatusCode::NOT_FOUND })
839            .with_state(state.clone())
840            .layer(
841                ServiceBuilder::new()
842                    .layer(CatchPanicLayer::custom(response_for_panic))
843                    .layer(RequestDecompressionLayer::new())
844                    .layer(CompressionLayer::new()),
845            )
846            .layer(
847                ServiceBuilder::new()
848                    .layer(SetRequestIdLayer::new(request_id.clone(), MakeRequestUuid))
849                    .layer(
850                        TraceLayer::new_for_http()
851                            .make_span_with(move |req: &Request<_>| {
852                                let request_id = req.headers().get(REQUEST_ID_HEADER);
853
854                                let ip = log_ips.then(|| {
855                                    req.extensions()
856                                        .get::<ConnectInfo<SocketAddr>>()
857                                        .map(|addr| tracing::field::display(addr.ip()))
858                                });
859
860                                let query = req.uri().query().map(tracing::field::display);
861
862                                server_span_clone.in_scope(|| match request_id {
863                                    Some(rid) => {
864                                        tracing::info_span!(
865                                            "admin",
866                                            domain = %api_domain_clone,
867                                            id = %rid
868                                                .to_str()
869                                                .unwrap_or(Uuid::new_v4().to_string().as_str()),
870                                            ip,
871                                            path = %req.uri().path(),
872                                            query,
873                                        )
874                                    }
875                                    None => {
876                                        tracing::info_span!(
877                                            "admin",
878                                            domain = %api_domain_clone,
879                                            id = %Uuid::new_v4(),
880                                            ip,
881                                            path = %req.uri().path(),
882                                            query,
883                                        )
884                                    }
885                                })
886                            })
887                            .on_request(move |req: &Request<_>, _: &Span| {
888                                let hd = log_headers
889                                    .then_some(HeadersDebug(req.headers(), redacted_hash.clone()));
890
891                                #[cfg(tracing_unstable)]
892                                let headers = log_headers.then_some(tracing::field::valuable(&hd));
893
894                                #[cfg(not(tracing_unstable))]
895                                let headers = log_headers.then_some(tracing::field::debug(&hd));
896
897                                tracing::info!(
898                                    version = ?req.version(),
899                                    method = %req.method(),
900                                    headers,
901                                    "req"
902                                );
903                            })
904                            .on_response(move |res: &Response<_>, latency: Duration, _: &Span| {
905                                let hd = log_headers.then_some(HeadersDebug(
906                                    res.headers(),
907                                    redacted_hash_clone.clone(),
908                                ));
909
910                                #[cfg(tracing_unstable)]
911                                let headers = log_headers.then_some(tracing::field::valuable(&hd));
912
913                                #[cfg(not(tracing_unstable))]
914                                let headers = log_headers.then_some(tracing::field::debug(&hd));
915
916                                let status = res.status().as_u16();
917                                let latency = LatencyDisplay(latency.as_nanos() as f64);
918
919                                if status >= 500 {
920                                    tracing::error!(status, headers, %latency, "res");
921                                } else if status >= 400 {
922                                    tracing::warn!(status, headers, %latency, "res");
923                                } else {
924                                    tracing::info!(status, headers, %latency, "res");
925                                }
926                            })
927                            .on_failure(
928                                |error: ServerErrorsFailureClass, _: Duration, _: &Span| {
929                                    tracing::error!(
930                                        err = %error,
931                                        "fail"
932                                    );
933                                },
934                            ),
935                    )
936                    .layer(TimeoutLayer::with_status_code(
937                        StatusCode::REQUEST_TIMEOUT,
938                        Duration::from_secs(10),
939                    ))
940                    .layer(PropagateRequestIdLayer::new(request_id))
941                    .layer(SetResponseHeaderLayer::if_not_present(
942                        header::SERVER,
943                        HeaderValue::from_static("Ordinary"),
944                    ))
945                    .layer(SetResponseHeaderLayer::overriding(
946                        header::ETAG,
947                        modify_etag_for_encoding,
948                    )),
949            );
950
951        if state.secure {
952            service = service.layer(SetResponseHeaderLayer::if_not_present(
953                header::STRICT_TRANSPORT_SECURITY,
954                HeaderValue::from_static("max-age=31536000"),
955            ));
956        }
957
958        if let Some(listener) = redirect_listener {
959            let api_domain = api_domain.clone();
960
961            let request_id_header = HeaderName::from_static(REQUEST_ID_HEADER);
962
963            let local_addr = listener.local_addr()?;
964            let state_clone = state.clone();
965
966            tokio::spawn(async move {
967                use axum::extract::State;
968
969                let span = server_span_clone3;
970
971                span.in_scope(
972                    || tracing::info!(server = %"redirect", addr = %local_addr, "listening"),
973                );
974
975                let handler = async move |uri: Uri,
976                                          headers: HeaderMap,
977                                          State(state): State<Arc<OrdinaryApiServerState>>|
978                            -> Result<Redirect, StatusCode> {
979                    // todo: handle localhost redirecting
980
981                    if let Some(host) = get_host(&headers, &uri) {
982                        if host == api_domain
983                            && let Some(path_and_query) = uri.path_and_query()
984                        {
985                            return Ok(Redirect::permanent(&format!(
986                                "https://{api_domain}{}",
987                                path_and_query.as_str()
988                            )));
989                        }
990
991                        let apps = state.apps.read();
992
993                        if apps.contains_key(&host)
994                            && let Some(path_and_query) = uri.path_and_query()
995                        {
996                            return Ok(Redirect::permanent(&format!(
997                                "https://{host}{}",
998                                path_and_query.as_str()
999                            )));
1000                        }
1001                    }
1002
1003                    Err(StatusCode::NOT_FOUND)
1004                };
1005
1006                let service = redirect_service(
1007                    span,
1008                    redacted_hash_clone1,
1009                    log_ips,
1010                    log_headers,
1011                    request_id_header,
1012                    handler,
1013                    state_clone,
1014                );
1015
1016                if let Err(err) = axum::serve(
1017                    listener,
1018                    service.into_make_service_with_connect_info::<SocketAddr>(),
1019                )
1020                .with_graceful_shutdown(shutdown_signal())
1021                .await
1022                {
1023                    tracing::error!(%err, "server closed");
1024                }
1025            });
1026        }
1027
1028        let local_addr = listener.local_addr()?;
1029
1030        let span = server_span_clone2;
1031
1032        match mode {
1033            SecurityMode::Insecure => {
1034                span.in_scope(|| {
1035                    tracing::info!(
1036                        server = %"primary",
1037                        addr = %local_addr,
1038                        provision = %"none",
1039                        "listening"
1040                    );
1041                });
1042
1043                self.start_apps(&span, &state)?;
1044
1045                axum::serve(
1046                    listener,
1047                    service.into_make_service_with_connect_info::<SocketAddr>(),
1048                )
1049                .with_graceful_shutdown(signal())
1050                .await?;
1051            }
1052            SecurityMode::Secure(cert_dir_path, provision) => {
1053                let span_clone = span.clone();
1054
1055                let (signal_tx, signal_rx) = watch::channel(());
1056                let (close_tx, close_rx) = watch::channel(());
1057
1058                {
1059                    let mut signal_tx_lock = state.signal_tx.write();
1060                    *signal_tx_lock = Some(signal_tx.clone());
1061
1062                    let mut close_rx_lock = state.close_rx.write();
1063                    *close_rx_lock = Some(close_rx.clone());
1064                }
1065
1066                let signal_tx_clone = state.signal_tx.clone();
1067
1068                tokio::spawn(async move {
1069                    signal().await;
1070
1071                    span_clone.in_scope(|| {
1072                        tracing::warn!("graceful shutdown");
1073                    });
1074
1075                    {
1076                        let mut signal_tx_lock = signal_tx_clone.write();
1077                        *signal_tx_lock = None;
1078                    }
1079
1080                    drop(signal_rx);
1081                });
1082
1083                match provision {
1084                    ProvisionMode::Localhost => {
1085                        let cert_dir_path = cert_dir_path.as_ref();
1086
1087                        ordinary_utils::generate_self_signed_localhost_certs(cert_dir_path)?;
1088
1089                        let rustls_config = rustls_server_config(
1090                            cert_dir_path.join("key.pem"),
1091                            cert_dir_path.join("crt.pem"),
1092                        )?;
1093
1094                        let tls_acceptor = TlsAcceptor::from(rustls_config);
1095                        span.in_scope(|| {
1096                            tracing::info!(
1097                                server = %"primary",
1098                                addr = %local_addr,
1099                                provision = %"localhost",
1100                                "listening"
1101                            );
1102                        });
1103
1104                        self.start_apps(&span, &state)?;
1105
1106                        loop {
1107                            let span = span.clone();
1108                            let (cnx, addr) = tokio::select! {
1109                                conn = listener.accept() => conn?,
1110                                () = signal_tx.closed() => {
1111                                    span.in_scope(|| {
1112                                       tracing::warn!("not accepting new connections");
1113                                    });
1114                                    break;
1115                                }
1116                            };
1117
1118                            let tower_service = service.clone();
1119                            let tls_acceptor = tls_acceptor.clone();
1120
1121                            let signal_tx = signal_tx.clone();
1122                            let close_rx = close_rx.clone();
1123
1124                            tokio::spawn(async move {
1125                                // todo: determine a better timeout
1126                                match timeout(Duration::from_secs(3), tls_acceptor.accept(cnx))
1127                                    .await
1128                                {
1129                                    Ok(stream) => {
1130                                        let stream = match stream {
1131                                            Ok(stream) => TokioIo::new(stream),
1132                                            Err(err) => {
1133                                                span.in_scope(|| {
1134                                                    if log_ips {
1135                                                        tracing::warn!(ip = %addr.ip(), port = addr.port(), %err, "accept");
1136                                                    } else {
1137                                                        tracing::warn!(%err, "accept");
1138                                                    }
1139                                                });
1140
1141                                                return;
1142                                            }
1143                                        };
1144
1145                                        OrdinaryAppServer::handle_stream(
1146                                            &span,
1147                                            tower_service,
1148                                            addr,
1149                                            stream,
1150                                            &signal_tx,
1151                                            &close_rx,
1152                                        )
1153                                        .await;
1154                                    }
1155                                    Err(_) => {
1156                                        span.in_scope(|| {
1157                                            if log_ips {
1158                                                tracing::warn!(ip = %addr.ip(), port = addr.port(), "timeout");
1159                                            } else {
1160                                                tracing::warn!("timeout");
1161                                            }
1162                                        });
1163                                    }
1164                                }
1165                            });
1166                        }
1167                    }
1168                    ProvisionMode::Staging | ProvisionMode::Production => {
1169                        let contacts = api_contacts
1170                            .iter()
1171                            .map(|c| format!("mailto:{c}"))
1172                            .collect::<Vec<String>>();
1173
1174                        let cache_path = Path::new(cert_dir_path.as_ref()).join("cache");
1175
1176                        let acme_state = AcmeConfig::new([&api_domain])
1177                            .contact(contacts)
1178                            .cache_option(Some(DirCache::new(cache_path)))
1179                            .directory_lets_encrypt(provision == ProvisionMode::Production)
1180                            .state();
1181
1182                        let challenge_rustls_config = acme_state.challenge_rustls_config();
1183                        let mut default_rustls_config =
1184                            ServerConfig::builder_with_provider(Arc::new(ring::default_provider()))
1185                                .with_safe_default_protocol_versions()?
1186                                .with_no_client_auth()
1187                                .with_cert_resolver(acme_state.resolver());
1188
1189                        default_rustls_config.alpn_protocols =
1190                            vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1191
1192                        let default_rustls_config = Arc::new(default_rustls_config);
1193
1194                        let api_domain_clone = api_domain.clone();
1195                        let span_clone = span.clone();
1196
1197                        let provision_str = match provision {
1198                            ProvisionMode::Production => "production",
1199                            ProvisionMode::Staging => "staging",
1200                            _ => unreachable!(),
1201                        };
1202
1203                        let acme_span = span_clone.in_scope(|| {
1204                            tracing::info_span!(
1205                                "acme",
1206                                domain = %api_domain_clone,
1207                                provision = %provision_str
1208                            )
1209                        });
1210
1211                        self.start_apps(&span, &state)?;
1212                        ordinary_utils::acme_task(acme_span.clone(), acme_state, signal_tx.clone());
1213
1214                        span.in_scope(|| {
1215                            tracing::info!(
1216                                server = %"primary",
1217                                addr = %local_addr,
1218                                provision = %provision_str,
1219                                "listening"
1220                            );
1221                        });
1222
1223                        let api_domain = Arc::new(api_domain);
1224
1225                        loop {
1226                            let span = span.clone();
1227                            let (cnx, addr) = tokio::select! {
1228                                conn = listener.accept() => conn?,
1229                                () = signal_tx.closed() => {
1230                                    span.in_scope(|| {
1231                                       tracing::warn!("not accepting new connections");
1232                                    });
1233                                    break;
1234                                }
1235                            };
1236
1237                            let tower_service = service.clone();
1238                            let acme_span = acme_span.clone();
1239
1240                            let challenge_rustls_config = challenge_rustls_config.clone();
1241                            let default_rustls_config = default_rustls_config.clone();
1242
1243                            let api_domain = api_domain.clone();
1244                            let apps = self.apps.clone();
1245
1246                            let signal_tx = signal_tx.clone();
1247                            let close_rx = close_rx.clone();
1248
1249                            tokio::spawn(async move {
1250                                // todo: determine a better timeout
1251                                match timeout(
1252                                    Duration::from_secs(3),
1253                                    LazyConfigAcceptor::new(Acceptor::default(), cnx),
1254                                )
1255                                .await
1256                                {
1257                                    Ok(start_handshake) => {
1258                                        let start_handshake = match start_handshake {
1259                                            Ok(start_handshake) => start_handshake,
1260                                            Err(err) => {
1261                                                if log_ips {
1262                                                    let tls_span = span.in_scope(|| { tracing::error_span!("tls", ip = %addr.ip(), port = addr.port()) });
1263
1264                                                    tls_span.in_scope(
1265                                                        || tracing::error!(%err, "accept"),
1266                                                    );
1267                                                } else {
1268                                                    let tls_span = span
1269                                                        .in_scope(|| tracing::error_span!("tls"));
1270
1271                                                    tls_span.in_scope(
1272                                                        || tracing::error!(%err, "accept"),
1273                                                    );
1274                                                }
1275
1276                                                return;
1277                                            }
1278                                        };
1279
1280                                        if let Some(sni) =
1281                                            start_handshake.client_hello().server_name()
1282                                        {
1283                                            let tls_span = span.in_scope(|| {
1284                                                if log_ips {
1285                                                    tracing::info_span!("tls", %sni, ip = %addr.ip(), port = addr.port())
1286                                                } else {
1287                                                    tracing::info_span!("tls", %sni)
1288                                                }
1289                                            });
1290
1291                                            if sni == *api_domain {
1292                                                if let Err(err) =
1293                                                    OrdinaryAppServer::handle_handshake(
1294                                                        &tls_span,
1295                                                        &acme_span,
1296                                                        start_handshake,
1297                                                        tower_service,
1298                                                        addr,
1299                                                        challenge_rustls_config,
1300                                                        default_rustls_config,
1301                                                        &signal_tx,
1302                                                        &close_rx,
1303                                                    )
1304                                                    .await
1305                                                {
1306                                                    tls_span.in_scope(|| {
1307                                                        tracing::error!(%err, "handshake");
1308                                                    });
1309                                                }
1310                                            } else {
1311                                                let apps = apps.read();
1312
1313                                                if let Some(app) = apps.get(sni) {
1314                                                    // todo: WrappedApp.live: Arc<RwLock<bool>>
1315                                                    // todo: just handle the stream here
1316                                                    if let Err(err) = app.stream_tx.send((
1317                                                        start_handshake,
1318                                                        addr,
1319                                                        tls_span.clone(),
1320                                                    )) {
1321                                                        tls_span.in_scope(
1322                                                            || tracing::error!(%err, "handoff"),
1323                                                        );
1324                                                    }
1325                                                } else {
1326                                                    tls_span
1327                                                        .in_scope(|| tracing::warn!("no match"));
1328                                                }
1329                                            }
1330                                        } else {
1331                                            let tls_span = span.in_scope(|| {
1332                                                if log_ips {
1333                                                    tracing::info_span!("tls", ip = %addr.ip(), port = addr.port())
1334                                                } else {
1335                                                    tracing::info_span!("tls")
1336                                                }
1337                                            });
1338
1339                                            tls_span.in_scope(|| tracing::warn!("no sni"));
1340                                        }
1341                                    }
1342                                    Err(_) => {
1343                                        if log_ips {
1344                                            let tls_span = span.in_scope(|| { tracing::error_span!("tls", ip = %addr.ip(), port = addr.port()) });
1345
1346                                            tls_span.in_scope(|| tracing::error!("timeout"));
1347                                        } else {
1348                                            let tls_span =
1349                                                span.in_scope(|| tracing::error_span!("tls"));
1350
1351                                            tls_span.in_scope(|| tracing::error!("timeout"));
1352                                        }
1353                                    }
1354                                }
1355                            });
1356                        }
1357                    }
1358                }
1359
1360                {
1361                    let mut close_rx_lock = state.close_rx.write();
1362                    *close_rx_lock = None;
1363                }
1364
1365                drop(close_rx);
1366                drop(listener);
1367
1368                span.in_scope(|| {
1369                    tracing::warn!(
1370                        "waiting for {} task(s) to finish",
1371                        close_tx.receiver_count()
1372                    );
1373                });
1374                close_tx.closed().await;
1375            }
1376        }
1377
1378        Ok(())
1379    }
1380
1381    fn start_apps(
1382        &self,
1383        server_span: &Span,
1384        state: &Arc<OrdinaryApiServerState>,
1385    ) -> anyhow::Result<()> {
1386        let state_clone = state.clone();
1387
1388        let Ok(shared_rt) = tokio::runtime::Handle::try_current() else {
1389            bail!("failed to get shared runtime");
1390        };
1391
1392        server_span.in_scope(|| -> anyhow::Result<()> {
1393            // todo: sleep this for 2 seconds or something to wait for the server to get started up
1394
1395            let mut apps = state_clone.apps.write();
1396
1397            for entry in fs::read_dir(&self.apps_dir)?.flatten() {
1398                let config_path = entry.path().join("ordinary.json");
1399
1400                match fs::read_to_string(&config_path) {
1401                    Ok(config_str) => match serde_json::from_str::<OrdinaryConfig>(&config_str) {
1402                        Ok(config) => {
1403                            let domain = config.domain.clone();
1404                            let app_span = tracing::info_span!("app", %domain);
1405
1406                            match ops::app::dbs(&state_clone, &config, &entry.path(), Some(app_span.clone())) {
1407                                Ok((auth, storage)) => {
1408                                    match ops::app::start(
1409                                        &state_clone,
1410                                        &config,
1411                                        auth,
1412                                        storage,
1413                                        match state_clone
1414                                            .provision_mode
1415                                            .as_ref()
1416                                            .unwrap_or(&ProvisionMode::Localhost)
1417                                        {
1418                                            ProvisionMode::Localhost => {
1419                                                ordinary_app::server::ProvisionMode::Localhost
1420                                            }
1421                                            ProvisionMode::Staging => {
1422                                                ordinary_app::server::ProvisionMode::Staging
1423                                            }
1424                                            ProvisionMode::Production => {
1425                                                ordinary_app::server::ProvisionMode::Production
1426                                            }
1427                                        },
1428                                        entry.path().join("certs"),
1429                                        shared_rt.clone(),
1430                                        Some(app_span),
1431                                    ) {
1432                                        Ok((port, app, kill_tx, stream_tx)) => {
1433                                            let cnames = app.config.cnames.clone();
1434
1435                                            let static_secret = StaticSecret::random_from_rng(OsRng);
1436                                            let public_key = PublicKey::from(&static_secret);
1437
1438                                            let app = Arc::new(WrappedOrdinaryAppServer {
1439                                                port,
1440                                                app,
1441                                                kill_tx,
1442                                                stream_tx,
1443                                                secrets_public_keypair: (static_secret, public_key),
1444                                            });
1445
1446                                            if let Some(cnames) = cnames {
1447                                                for cname in cnames {
1448                                                    apps.insert(cname, app.clone());
1449                                                }
1450                                            }
1451
1452                                            apps.insert(
1453                                                domain,
1454                                                app,
1455                                            );
1456                                        }
1457                                        Err(err) => {
1458                                            tracing::error!(%err, domain, "failed to start app");
1459                                        }
1460                                    }
1461                                }
1462                                Err(err) => {
1463                                    tracing::error!(%err, domain, "failed to init dbs");
1464                                }
1465                            }
1466                        }
1467                        Err(err) => {
1468                            if let Some(path_str) = config_path.to_str() {
1469                                tracing::error!(%err, path = path_str, "failed to parse config file");
1470                            } else {
1471                                tracing::error!(%err, "failed to parse config file");
1472                            }
1473                        }
1474                    },
1475                    Err(err) => {
1476                        if let Some(path_str) = config_path.to_str() {
1477                            tracing::warn!(%err, path = path_str, "failed to read config file");
1478                        } else {
1479                            tracing::warn!(%err, "failed to read config file");
1480                        }
1481                    }
1482                }
1483            }
1484
1485            drop(apps);
1486
1487            Ok(())
1488        })
1489    }
1490}