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