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