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