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
5pub(crate) mod apps;
6pub(crate) mod auth;
7pub(crate) mod openapi;
8pub(crate) mod ops;
9mod start;
10
11use hashbrown::{HashMap, HashSet};
12use ordinary_app::server::OrdinaryAppServer;
13#[cfg(feature = "server")]
14use ordinary_config::OrdinaryApiLimits;
15use ordinary_monitor::service::OrdinaryMonitorService;
16use ordinary_storage::Storage;
17use parking_lot::{Mutex, RwLock};
18use rand_chacha::rand_core::Rng;
19use rand_chacha::rand_core::SeedableRng;
20use saferlmdb::{EnvBuilder, Environment};
21use sha2::{Digest, Sha256};
22use std::io::Write;
23use std::process;
24use tokio::net::TcpListener;
25use tokio::net::TcpStream;
26use tokio_rustls::StartHandshake;
27
28use axum::Router;
29
30use std::net::SocketAddr;
31use std::path::Path;
32use std::path::PathBuf;
33use std::sync::Arc;
34
35use tokio_rustls::rustls::ServerConfig;
36use tracing::{Instrument, Span};
37
38use ordinary_auth::{Auth, AuthClient};
39
40use std::fs;
41
42use crate::server::auth::AccountLockManager;
43use crate::server::start::start;
44use crate::{api_account_claims, api_invite_claims};
45use anyhow::bail;
46use getrandom::SysRng;
47use ordinary_config::{
48    AccessTokenConfig, AuthConfig, InviteConfig, InviteMode, MfaConfig, OrdinaryApiConfig,
49    PasswordConfig, RedactedHashAlg, RefreshTokenConfig, TotpConfig,
50};
51use ordinary_monitor::tracing::logger::OrdinaryLogger;
52use ordinary_utils::{ProvisionMode, SecurityMode};
53use sysinfo::{Pid, System};
54use tokio::sync::watch::{Receiver, Sender};
55use x25519_dalek::{PublicKey, StaticSecret};
56
57pub struct WrappedOrdinaryAppServer {
58    port: u16,
59    app: Arc<OrdinaryAppServer>,
60    terminate_tx: Sender<bool>,
61    stream_tx: tokio::sync::mpsc::UnboundedSender<(StartHandshake<TcpStream>, SocketAddr, Span)>,
62    dh_keypair: (StaticSecret, PublicKey),
63}
64
65pub struct WrappedOrdinaryProxyServer {
66    service: Router,
67    terminate_rx: Receiver<bool>,
68    /// (challenge, default)
69    configs: Option<(Arc<ServerConfig>, Arc<ServerConfig>)>,
70}
71
72type Dbs = Arc<tokio::sync::Mutex<HashMap<String, (Arc<Environment>, Arc<Auth>, Arc<Storage>)>>>;
73
74pub enum Server {
75    App(Arc<WrappedOrdinaryAppServer>),
76    Proxy(Arc<WrappedOrdinaryProxyServer>),
77}
78
79type AppServers = Arc<tokio::sync::RwLock<HashMap<String, Server>>>;
80
81#[allow(clippy::struct_excessive_bools)]
82pub struct OrdinaryApiServerState {
83    pub domain: String,
84    pub app_domains: Arc<Vec<String>>,
85    pub secure: bool,
86    pub secure_cookies: bool,
87    pub log_headers: bool,
88    pub log_ips: bool,
89    pub log_size: bool,
90    pub auth: Arc<Auth>,
91    pub servers: AppServers,
92    pub apps_dir: PathBuf,
93    pub dbs: Dbs,
94    pub env_name: String,
95    pub provision_mode: Option<ProvisionMode>,
96    pub dedicated_ports: bool,
97    pub server_span: Span,
98    pub monitor: Arc<Option<OrdinaryMonitorService>>,
99    pub stored_logs: bool,
100    pub limits: String,
101    pub config: OrdinaryApiConfig,
102
103    pub signal_tx: Arc<RwLock<Option<Sender<()>>>>,
104    pub close_rx: Arc<RwLock<Option<Receiver<()>>>>,
105
106    pub privileged_domains: HashSet<String>,
107
108    pub pid: Pid,
109    pub system: Arc<Mutex<System>>,
110
111    pub account_lock_manager: Arc<AccountLockManager>,
112
113    pub danger_dns_no_verify: bool,
114}
115
116pub struct OrdinaryApiServer {
117    auth: Arc<Auth>,
118    env: Arc<Environment>,
119    config: OrdinaryApiConfig,
120    servers: AppServers,
121    apps_dir: PathBuf,
122    monitor: Arc<Option<OrdinaryMonitorService>>,
123}
124
125#[cfg(feature = "server")]
126impl OrdinaryApiServer {
127    #[allow(clippy::similar_names, clippy::too_many_arguments)]
128    pub async fn init(
129        env_name: &str,
130        domain: &str,
131        password: &str,
132        env_path: impl AsRef<Path>,
133        storage_size: usize,
134        api_contacts: &[String],
135        app_domains: &[String],
136        privileged_domains: &Option<Vec<String>>,
137        logger: Option<OrdinaryLogger>,
138    ) -> anyhow::Result<(String, Vec<u8>)> {
139        let span = tracing::info_span!("init", env = %env_name, pid = process::id());
140
141        let mut limits = OrdinaryApiLimits::default();
142        if let Some(pd) = privileged_domains {
143            pd.clone_into(&mut limits.privileged_domains);
144        }
145        limits.app_domains = app_domains.to_vec();
146
147        async {
148            let config = OrdinaryApiConfig {
149                domain: domain.into(),
150                contacts: api_contacts.to_vec(),
151                // todo: pass in as a flag
152                public_dns_ip: None,
153                env_name: env_name.to_string(),
154                limits,
155            };
156            let config_file = serde_json::to_string_pretty(&config)?;
157
158            let data_path = env_path.as_ref().join("data");
159
160            if data_path.exists() {
161                bail!("environment already initialized");
162            }
163
164            fs::write(env_path.as_ref().join("ordinaryd.json"), config_file)?;
165
166            let api_server =
167                OrdinaryApiServer::new(env_name, env_path, storage_size, logger).await?;
168
169            let mut input = domain.as_bytes().to_vec();
170            input.extend_from_slice(b"root");
171
172            let mut password_input = input.clone();
173            password_input.extend_from_slice(password.as_bytes());
174
175            let mut hasher = Sha256::new();
176            hasher.update(&password_input);
177            let password = hasher.finalize().to_vec();
178
179            let invite_token = api_server.auth.api_invite_get(domain, "root", None)?;
180
181            let (state, reg_start_req) = AuthClient::registration_start_req(b"root", &password)?;
182
183            let checked_claims = api_server.auth.invite_check(&invite_token)?;
184            let reg_start_res = api_server.auth.registration_start(
185                reg_start_req,
186                None,
187                None,
188                Some(checked_claims),
189            )?;
190
191            let (private_key, reg_finish_req) =
192                AuthClient::registration_finish_req(b"root", &password, &state, &reg_start_res)?;
193            let (reg_finish_res, ..) = api_server.auth.registration_finish(reg_finish_req, None)?;
194
195            let (totp, _recovery_codes) = AuthClient::decrypt_totp_mfa(
196                &reg_finish_res,
197                private_key,
198                env_name.to_string(),
199                "root".into(),
200            )?;
201
202            Ok((totp.get_url(), totp.secret))
203        }
204        .instrument(span.clone())
205        .await
206    }
207
208    #[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
209    pub async fn new(
210        env_name: &str,
211        env_path: impl AsRef<Path>,
212        storage_size: usize,
213        logger: Option<OrdinaryLogger>,
214    ) -> anyhow::Result<OrdinaryApiServer> {
215        let span = tracing::info_span!("setup", env = %env_name, pid = process::id());
216
217        async {
218            let config: OrdinaryApiConfig = serde_json::from_str(&fs_err::read_to_string(
219                env_path.as_ref().join("ordinaryd.json"),
220            )?)?;
221
222            let data_path = env_path.as_ref().join("data");
223
224            fs_err::create_dir_all(&data_path)?;
225
226            let ps = page_size::get();
227
228            // round up to full OS page
229            let remainder = storage_size % ps;
230            let mapsize = (storage_size - remainder) + ps;
231
232            tracing::info!(mapsize = %bytesize::ByteSize(mapsize as u64).display().si_short());
233
234            let env = Arc::new(unsafe {
235                let mut env_builder = EnvBuilder::new()?;
236                env_builder.set_maxreaders(126)?;
237                env_builder.set_mapsize(mapsize)?;
238                env_builder.set_maxdbs(13)?;
239                env_builder.open(
240                    match data_path.to_str() {
241                        Some(v) => v,
242                        None => bail!("data_path not a str"),
243                    },
244                    &saferlmdb::open::Flags::empty(),
245                    0o600,
246                )?
247            });
248
249            let keys_dir = env_path.as_ref().join("keys");
250            fs_err::create_dir_all(&keys_dir)?;
251
252            let auth_key_path = keys_dir.join("auth");
253
254            let auth_key: [u8; 32] = if auth_key_path.exists() && auth_key_path.is_file() {
255                let auth_key = fs_err::read(&auth_key_path)?;
256                let auth_key: [u8; 32] = auth_key[..].try_into()?;
257                auth_key
258            } else {
259                let mut auth_key = [0u8; 32];
260                let mut rng = rand_chacha::ChaCha20Rng::try_from_rng(&mut SysRng)?;
261
262                rng.fill_bytes(&mut auth_key[..]);
263
264                let mut auth_key_file = fs_err::File::create(auth_key_path)?;
265                auth_key_file.write_all(&auth_key)?;
266                auth_key_file.flush()?;
267
268                auth_key
269            };
270
271            let auth = Arc::new(Auth::new(
272                config.domain.clone(),
273                Some(AuthConfig {
274                    password: PasswordConfig {
275                        protocol: ordinary_config::PasswordProtocol::Opaque,
276                    },
277                    mfa: MfaConfig {
278                        totp: TotpConfig {
279                            template: None,
280                            algorithm: ordinary_config::TotpAlgorithm::Sha1,
281                        },
282                    },
283                    refresh_token: RefreshTokenConfig::default(),
284                    access_token: AccessTokenConfig {
285                        claims: api_account_claims(),
286                        ..AccessTokenConfig::default()
287                    },
288                    client_hash: ordinary_config::ClientPasswordHash::Sha256,
289                    cookies_enabled: true,
290                    invite: Some(InviteConfig {
291                        // todo: make `InviteMode` configurable
292                        mode: InviteMode::Root,
293                        lifetime: 60 * 60 * 24,
294                        clean_interval: (30, 90),
295                        claims: Some(api_invite_claims()),
296                    }),
297                }),
298                auth_key,
299                env.clone(),
300            )?);
301
302            let apps_dir = env_path.as_ref().join("apps");
303            fs_err::create_dir_all(&apps_dir)?;
304
305            let monitor = Arc::new(logger.map(|logger| {
306                OrdinaryMonitorService::new(logger).expect("failed to set up monitor service")
307            }));
308
309            Ok(OrdinaryApiServer {
310                config,
311                auth,
312                env,
313                servers: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
314                apps_dir,
315                monitor,
316            })
317        }
318        .instrument(span)
319        .await
320    }
321
322    #[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
323    pub async fn start<P, F>(
324        &self,
325        server_span: Span,
326        mode: SecurityMode<P>,
327        listener: TcpListener,
328        secure_cookies: bool,
329        log_headers: bool,
330        log_ips: bool,
331        log_size: bool,
332        redirect_listener: Option<TcpListener>,
333        dedicated_ports: bool,
334        stored_logs: bool,
335        redacted_hash: Option<RedactedHashAlg>,
336        openapi: bool,
337        swagger: bool,
338        signal: fn() -> F,
339        danger_dns_no_verify: bool,
340    ) -> anyhow::Result<()>
341    where
342        P: AsRef<Path> + std::clone::Clone,
343        F: Future<Output = ()> + Send + 'static,
344    {
345        start(
346            self,
347            server_span,
348            mode,
349            listener,
350            secure_cookies,
351            log_headers,
352            log_ips,
353            log_size,
354            redirect_listener,
355            dedicated_ports,
356            stored_logs,
357            redacted_hash,
358            openapi,
359            swagger,
360            signal,
361            danger_dns_no_verify,
362        )
363        .await
364    }
365}