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