1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![doc = include_str!("../docs/app-config-reference.md")]
4#![doc = include_str!("../docs/host-config-reference.md")]
5#![warn(clippy::all, clippy::pedantic)]
6#![allow(clippy::missing_errors_doc)]
7
8#[cfg(feature = "docs")]
13pub mod jsonschema;
14#[cfg(feature = "docs")]
15pub use schemars;
16
17mod app;
18pub mod auth;
19mod host;
20mod http;
21mod validate;
22
23pub use app::*;
24pub use auth::*;
25pub use host::*;
26pub use http::*;
27
28pub use crate::validate::DOMAIN_REGEX;
29use crate::validate::validate;
30
31use anyhow::bail;
32use hashbrown::{HashMap, HashSet};
33use serde::{Deserialize, Serialize};
34use smallvec::smallvec;
35use std::collections::BTreeMap;
36use std::fmt::Write;
37use std::path::Path;
38use std::process::Command;
39use std::{env, fs};
40use tracing::instrument;
41
42#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
43#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
44#[derive(Deserialize, Serialize, Debug, Clone)]
45pub struct ClientLoggingConfig {
46 #[serde(skip_serializing_if = "Option::is_none")]
48 #[serde(default)]
49 min_delay: Option<u32>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 #[serde(default)]
53 max_delay: Option<u32>,
54 #[serde(skip_serializing_if = "Option::is_none")]
57 #[serde(default)]
58 max_buffer: Option<u16>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 #[serde(default)]
62 max_batch: Option<u16>,
63}
64
65#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
66#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
67#[derive(Deserialize, Serialize, Debug, Clone)]
68pub enum RedactedHashAlg {
69 Blake2,
70 Blake3,
71}
72
73#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
74#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
75#[derive(Deserialize, Serialize, Debug, Clone)]
76pub struct ServerLoggingConfig {
77 #[serde(skip_serializing_if = "Option::is_none")]
78 #[serde(default)]
79 pub ips: Option<bool>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 #[serde(default)]
82 pub headers: Option<bool>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 #[serde(default)]
85 pub credentials: Option<RedactedHashAlg>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 #[serde(default)]
88 pub timing: Option<bool>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 #[serde(default)]
91 pub sizes: Option<bool>,
92}
93
94#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
95#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
96#[derive(Deserialize, Serialize, Debug, Clone)]
97pub struct LoggingConfig {
98 #[serde(skip_serializing_if = "Option::is_none")]
99 #[serde(default)]
100 pub client: Option<ClientLoggingConfig>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 #[serde(default)]
103 pub server: Option<ServerLoggingConfig>,
104}
105
106#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
108#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
109#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
110pub enum CompressionAlgorithm {
111 Uncompressed,
112 Gzip,
113 Zstd { level: u8 },
114 Brotli,
115 Deflate,
116 All,
117}
118
119impl CompressionAlgorithm {
120 #[must_use]
121 pub fn as_u8(&self) -> u8 {
122 match self {
123 Self::Uncompressed => 0,
124 Self::Gzip => 1,
125 Self::Zstd { level: _ } => 2,
126 Self::Brotli => 3,
127 Self::Deflate => 4,
128 Self::All => 255,
129 }
130 }
131
132 #[must_use]
133 pub fn from_u8(val: u8, lvl: Option<u8>) -> Self {
134 match val {
135 0 => Self::Uncompressed,
136 1 => Self::Gzip,
137 2 => Self::Zstd {
138 level: lvl.unwrap_or(17),
139 },
140 3 => Self::Brotli,
141 4 => Self::Deflate,
142 _ => Self::All,
143 }
144 }
145
146 #[must_use]
147 pub fn as_char(&self) -> char {
148 match self {
149 Self::Uncompressed => '0',
150 Self::Gzip => '1',
151 Self::Zstd { level: _ } => '2',
152 Self::Brotli => '3',
153 Self::Deflate => '4',
154 Self::All => 'A',
155 }
156 }
157
158 #[must_use]
159 pub fn as_str(&self) -> &'static str {
160 match self {
161 Self::Uncompressed => "uncompressed",
162 Self::Gzip => "gzip",
163 Self::Zstd { level: _ } => "zstd",
164 Self::Brotli => "br",
165 Self::Deflate => "deflate",
166 Self::All => "all",
167 }
168 }
169}
170
171#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
172#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
173#[derive(Deserialize, Serialize, Debug, Clone, Default)]
174pub struct ErrorConfig {
175 #[serde(skip_serializing_if = "Option::is_none")]
179 #[serde(default)]
180 pub asset: Option<String>,
181}
182
183#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
184#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
185#[derive(Deserialize, Serialize, Debug, Clone)]
186pub enum RuntimeMode {
187 Shared,
190 SingleThreaded,
193 MultiThreaded,
196}
197
198#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
199#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
200#[derive(Deserialize, Serialize, Debug, Clone)]
201pub struct LifecycleBeforeAfterScripts {
202 #[serde(skip_serializing_if = "Option::is_none")]
203 #[serde(default)]
204 pub before: Option<Vec<Vec<String>>>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 #[serde(default)]
207 pub after: Option<Vec<Vec<String>>>,
208}
209
210#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
211#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
212#[derive(Deserialize, Serialize, Debug, Clone)]
213pub struct TopLevelLifecycle {
214 pub before_all: Option<Vec<Vec<String>>>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 #[serde(default)]
220 pub build: Option<LifecycleBeforeAfterScripts>,
221}
222
223#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
225#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
226#[derive(Deserialize, Serialize, Debug, Clone, Default)]
227pub struct OrdinaryConfig {
228 #[serde(skip_serializing_if = "Option::is_none")]
229 #[serde(default)]
230 pub lifecycle: Option<TopLevelLifecycle>,
231
232 pub domain: String,
235
236 pub version: String,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
245 #[serde(default)]
246 pub cnames: Option<Vec<String>>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
258 #[serde(default)]
259 pub canonical: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
262 #[serde(default)]
263 pub http: Option<HttpConfig>,
264
265 #[serde(skip)]
266 #[serde(default)]
267 pub internal_middlewares: Option<HashMap<String, MiddlewareConfig>>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
272 #[serde(default)]
273 pub contacts: Option<Vec<String>>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 #[serde(default)]
278 pub hide_contacts: Option<bool>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 #[serde(default = "OrdinaryConfig::default_storage_size")]
283 pub storage_size: Option<u64>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
289 #[serde(default)]
290 pub runtime: Option<RuntimeMode>,
291
292 #[serde(skip_serializing_if = "Option::is_none")]
298 #[serde(default)]
299 pub hide_schema: Option<bool>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 #[serde(default)]
304 pub client_events: Option<bool>,
305
306 #[serde(skip_serializing_if = "Option::is_none")]
308 #[serde(default)]
309 pub port: Option<u16>,
310 #[serde(skip_serializing_if = "Option::is_none")]
313 #[serde(default)]
314 pub redirect_port: Option<u16>,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
317 #[serde(default)]
318 pub logging: Option<LoggingConfig>,
319 #[serde(skip_serializing_if = "Option::is_none")]
324 #[serde(default)]
325 pub error: Option<ErrorConfig>,
326 #[serde(skip_serializing_if = "Option::is_none")]
328 #[serde(default)]
329 pub auth: Option<AuthConfig>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 #[serde(default)]
333 pub globals: Option<BTreeMap<String, String>>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 #[serde(default)]
337 pub secrets: Option<Vec<Secret>>,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 #[serde(default)]
341 pub database: Option<DatabaseConfig>,
342 #[serde(skip_serializing_if = "Option::is_none")]
345 #[serde(default)]
346 pub functions: Option<Vec<FunctionConfig>>,
347 #[serde(skip_serializing_if = "Option::is_none")]
351 #[serde(default)]
352 pub assets: Option<AssetsConfig>,
353}
354
355impl OrdinaryConfig {
356 pub fn get(proj_path: impl AsRef<Path>, load_refs: bool) -> anyhow::Result<OrdinaryConfig> {
358 let path = proj_path.as_ref().join("ordinary.json");
359 let mut config_json = fs::read(&path)?;
361
362 let mut config = match simd_json::from_slice::<OrdinaryConfig>(&mut config_json) {
363 Ok(config) => config,
364 Err(err) => bail!("{}: {err}", path.display()),
365 };
366
367 if load_refs {
368 config.load_refs(proj_path.as_ref())?;
369 }
370
371 config.load_internal();
372
373 if let Some(database_config) = config.database.as_mut() {
374 database_config.models.sort_by_key(|m| m.idx);
375
376 for model_config in &mut database_config.models {
377 model_config.fields.sort_by_key(|m| m.idx);
378
379 for field in &mut model_config.fields {
380 field.kind.sort_sub_fields();
381 }
382 }
383 }
384
385 Ok(config)
386 }
387
388 pub fn write(&self, proj_path: &Path) -> anyhow::Result<()> {
389 use std::io::Write;
390
391 let ordinary_json = serde_json::to_string_pretty(self)?;
392
393 let mut file = fs::File::create(proj_path.join("ordinary.json"))?;
394 file.write_all(ordinary_json.as_bytes())?;
395
396 Ok(())
397 }
398
399 fn load_refs(&mut self, proj_path: &Path) -> anyhow::Result<()> {
400 if let Some(functions) = self.functions.as_mut() {
401 Self::load_function_refs(proj_path, functions)?;
402 }
403
404 Ok(())
405 }
406
407 fn load_function_refs(
408 proj_path: &Path,
409 function_configs: &mut Vec<FunctionConfig>,
410 ) -> anyhow::Result<()> {
411 for base_function_config in function_configs {
412 if let Some(reference) = &base_function_config.r#ref {
413 let json_path = proj_path.join(reference);
414 let mut json_bytes = fs_err::read(json_path)?;
415
416 let mut ref_function_config: FunctionConfig =
417 simd_json::from_slice(json_bytes.as_mut_slice())?;
418 ref_function_config.load_bindgen();
419
420 ref_function_config
421 .r#ref
422 .clone_from(&base_function_config.r#ref);
423
424 if let Some(name) = &base_function_config.name {
425 ref_function_config.name = Some(name.clone());
426 }
427
428 if let Some(timeout) = base_function_config.timeout {
429 ref_function_config.timeout = Some(timeout);
430 }
431
432 *base_function_config = ref_function_config;
433 }
434 }
435
436 Ok(())
437 }
438
439 #[must_use]
440 pub fn database_model_map(&self) -> HashMap<String, DatabaseModelConfig> {
441 let mut model_map = HashMap::new();
442
443 let mut database_config = self.database.clone();
444
445 if let Some(database_config) = database_config.as_mut() {
446 database_config.models.sort_by_key(|m| m.idx);
447
448 for model_config in &mut database_config.models {
449 model_config.fields.sort_by_key(|m| m.idx);
450
451 for field in &mut model_config.fields {
452 field.kind.sort_sub_fields();
453 }
454 }
455 }
456
457 if let Some(database_config) = database_config {
458 for model_config in &database_config.models {
459 model_map.insert(model_config.name.clone(), model_config.clone());
460 }
461 }
462
463 model_map
464 }
465
466 pub fn load_internal(&mut self) {
467 self.canonical = Some(
468 self.canonical.clone().unwrap_or(
469 self.cnames
470 .clone()
471 .unwrap_or_default()
472 .first()
473 .map(ToOwned::to_owned)
474 .unwrap_or(self.domain.clone()),
475 ),
476 );
477
478 self.load_internal_middlewares();
479 self.load_internal_compression();
480 self.load_internal_content_types();
481 }
482
483 fn load_internal_middlewares(&mut self) {
484 if let Some(http_config) = &self.http
485 && let Some(middlewares) = &http_config.middlewares
486 {
487 let mut map = HashMap::new();
488
489 for middleware in middlewares {
490 map.insert(middleware.name.clone(), middleware.clone());
491 }
492
493 self.internal_middlewares = Some(map);
494 }
495 }
496
497 fn load_internal_compression(&mut self) {
498 if let Some(assets) = self.assets.as_mut()
499 && let Some(precompression) = &assets.precompression
500 {
501 assets.internal_precompression = Some(precompression.get_list());
502 }
503
504 if let Some(http_config) = self.http.as_mut()
505 && let Some(http_routes) = http_config.routes.as_mut()
506 {
507 for http_route in http_routes {
508 if let Some(http_config) = http_route.config.as_mut()
509 && let Some(http_cache) = http_config.cache.as_mut()
510 && let Some(stored_cache) = http_cache.stored.as_mut()
511 && let Some(compression) = &stored_cache.compression
512 {
513 stored_cache.internal_compressions = Some(compression.get_list());
514 }
515 }
516 }
517 }
518
519 fn load_internal_content_types(&mut self) {
520 if let Some(http_config) = self.http.as_mut()
521 && let Some(http_routes) = http_config.routes.as_mut()
522 {
523 for http_route in http_routes {
524 if let Some(http_config) = http_route.config.as_mut()
525 && let Some(http_cache) = http_config.cache.as_mut()
526 && let Some(stored_cache) = http_cache.stored.as_mut()
527 {
528 stored_cache.internal_content_types = Some(
529 stored_cache
530 .content_types
531 .clone()
532 .unwrap_or(smallvec!["text/html".into(), "application/json".into()]),
533 );
534 }
535 }
536 }
537 }
538
539 #[must_use]
540 pub fn get_middlewares(&self, middleware_names: &Vec<String>) -> Option<Vec<MiddlewareConfig>> {
541 let mut middleware_configs = vec![];
542
543 if let Some(middleware_map) = &self.internal_middlewares {
544 for middleware in middleware_names {
545 if let Some(middleware_config) = middleware_map.get(middleware) {
546 middleware_configs.push(middleware_config.clone());
547 }
548 }
549 }
550
551 if !middleware_configs.is_empty() {
552 return Some(middleware_configs);
553 }
554
555 None
556 }
557
558 pub fn for_send(&self) -> anyhow::Result<OrdinaryConfig> {
561 let mut config = self.clone();
562
563 config.lifecycle = None;
564
565 if let Some(assets) = config.assets.as_mut() {
566 assets.dir_path = None;
567 }
568
569 if let Some(function_configs) = config.functions.as_mut() {
570 for function_config in function_configs {
571 function_config.r#ref = None;
572
573 function_config.build = None;
574 function_config.bin = None;
575 function_config.bindgen = None;
576 }
577 }
578
579 Ok(config)
580 }
581
582 #[instrument(skip_all, err, level = "debug")]
585 pub fn validate(&self) -> anyhow::Result<()> {
586 validate(self)
587 }
588
589 #[must_use]
591 #[allow(clippy::unnecessary_wraps)]
592 pub fn default_storage_size() -> Option<u64> {
593 Some(5_000_000)
594 }
595 #[allow(clippy::too_many_lines)]
602 pub fn check_config_against_limits(
603 &self,
604 limits: &OrdinaryHostLimits,
605 privileged_domains: &HashSet<String>,
606 ) -> anyhow::Result<()> {
607 check_config_against_limits(self, limits, privileged_domains)
608 }
609
610 pub fn exec_script(
611 proj_path: &Path,
612 argument: &Option<String>,
613 name: &str,
614 when: &str,
615 scripts: &Vec<Vec<String>>,
616 ) -> anyhow::Result<()> {
617 let span = tracing::info_span!("lifecycle", %when, %name);
618
619 span.in_scope(|| {
620 exec_script(proj_path, argument, scripts)?;
621 anyhow::Ok(())
622 })
623 }
624
625 pub fn check_function_name_exists(&self, name: &str) -> anyhow::Result<()> {
626 if let Some(function_configs) = &self.functions {
627 for function_config in function_configs {
628 if function_config.name.as_deref() == Some(name) {
629 bail!("function with name {name} already exists");
630 }
631 }
632 }
633
634 Ok(())
635 }
636}
637
638pub fn exec_script(
639 proj_path: &Path,
640 argument: &Option<String>,
641 scripts: &Vec<Vec<String>>,
642) -> anyhow::Result<()> {
643 let curr_dir = env::current_dir()?;
644 env::set_current_dir(proj_path)?;
645
646 for script in scripts {
647 let mut script_iter = script.iter();
648
649 if let Some(command) = script_iter.next() {
650 let mut command_str = command.clone();
651 let mut command = Command::new(command);
652
653 for arg in script_iter {
654 write!(command_str, " {arg}")?;
655 command.arg(arg);
656 }
657
658 tracing::info!(cmd = %command_str, "exec");
659
660 let output = match &argument {
661 Some(arg) => command.arg(arg).output()?,
662 None => command.output()?,
663 };
664
665 if !output.status.success() {
666 let stderr = str::from_utf8(&output.stderr)?;
667 let stdout = str::from_utf8(&output.stdout)?;
668
669 tracing::error!(%stderr, %stdout, "failed");
670 bail!(stderr.to_string());
671 }
672 }
673 }
674
675 env::set_current_dir(curr_dir)?;
676
677 Ok(())
678}