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