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