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