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