1pub(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 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 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, ®_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 ®_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 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 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}