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