#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![doc = include_str!("../docs/config-reference.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
mod validate;
pub use crate::validate::DOMAIN_REGEX;
use crate::validate::validate;
use anyhow::bail;
use arrayvec::ArrayVec;
use hashbrown::HashSet;
use ordinary_types::{Field, Kind};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::env;
use std::fmt::{Display, Formatter, Write};
use std::path::Path;
use std::process::Command;
use tracing::instrument;
fn default_env_name() -> String {
"development".to_string()
}
#[derive(Deserialize, Serialize, Clone)]
pub struct OrdinaryApiConfig {
pub domain: String,
pub contacts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub public_dns_ip: Option<[u8; 4]>,
#[serde(default = "default_env_name")]
pub env_name: String,
#[serde(default)]
pub limits: OrdinaryApiLimits,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AssetsLimits {
pub allowed_extensions: Vec<String>,
pub max_store_size: u64,
pub max_asset_size: u64,
}
impl Default for AssetsLimits {
fn default() -> Self {
Self {
allowed_extensions: vec![
"otf".into(),
"ttf".into(),
"woff".into(),
"woff2".into(),
"txt".into(),
"xml".into(),
"html".into(),
"css".into(),
"css.map".into(),
"csv".into(),
"js".into(),
"png".into(),
"apng".into(),
"gif".into(),
"svg".into(),
"jpg".into(),
"jpeg".into(),
"bmp".into(),
"tif".into(),
"tiff".into(),
"webp".into(),
"avif".into(),
"ico".into(),
"pdf".into(),
"json".into(),
"wasm".into(),
],
max_store_size: 100_000_000,
max_asset_size: 1_500_000,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ArtifactLimits {
pub max_store_size: u64,
pub max_artifact_size: u64,
}
impl Default for ArtifactLimits {
fn default() -> Self {
Self {
max_store_size: 10_000_000,
max_artifact_size: 500_000,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct CacheLimits {
pub max_size_range: (u64, u64),
pub max_count_range: (usize, usize),
pub clean_interval_ranges: ((u64, u64), (u64, u64)),
}
impl Default for CacheLimits {
fn default() -> Self {
Self {
max_size_range: (1_000_000, 10_000_000),
max_count_range: (100, 500),
clean_interval_ranges: ((5, 10), (15, 20)),
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentLimits {
pub search_enabled: bool,
pub max_content_definitions: u8,
pub max_content_fields: u8,
pub max_store_size: u64,
pub max_object_size: u64,
pub max_field_size: u64,
}
impl Default for ContentLimits {
fn default() -> Self {
Self {
search_enabled: true,
max_content_definitions: 255,
max_content_fields: 255,
max_store_size: 10_000_000,
max_object_size: 400_000,
max_field_size: 200_000,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ModelLimits {
pub search_enabled: bool,
pub max_model_definitions: u8,
pub max_model_fields: u8,
pub max_item_size: u64,
pub max_field_size: u64,
}
impl Default for ModelLimits {
fn default() -> Self {
Self {
search_enabled: true,
max_model_definitions: 255,
max_model_fields: 255,
max_item_size: 400_000,
max_field_size: 100_000,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct SecretsLimits {
pub max_count: u8,
pub max_size: u64,
}
impl Default for SecretsLimits {
fn default() -> Self {
Self {
max_count: 225,
max_size: 2_000,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct StorageLimits {
pub max_storage: u64,
pub max_app_storage: u64,
pub assets: AssetsLimits,
pub artifact: ArtifactLimits,
pub cache: CacheLimits,
pub content: ContentLimits,
pub model: ModelLimits,
pub secrets: SecretsLimits,
}
impl Default for StorageLimits {
fn default() -> Self {
Self {
max_storage: 20_000_000_000,
max_app_storage: 50_000_000,
assets: AssetsLimits::default(),
artifact: ArtifactLimits::default(),
cache: CacheLimits::default(),
content: ContentLimits::default(),
model: ModelLimits::default(),
secrets: SecretsLimits::default(),
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct MonitorLimits {}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IntegrationLimits {
pub count: u8,
pub max_timeout: u16,
}
impl Default for IntegrationLimits {
fn default() -> Self {
Self {
count: 255,
max_timeout: 10,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionLimits {
pub count: u8,
pub max_timeout: u16,
}
impl Default for ActionLimits {
fn default() -> Self {
Self {
count: 255,
max_timeout: 10,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AuthLimits {}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProxyLimits {
pub count: u8,
pub disallowed_targets: Vec<String>,
}
impl Default for ProxyLimits {
fn default() -> Self {
Self {
count: 255,
disallowed_targets: vec![],
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateLimits {
pub count: u8,
pub max_timeout: u16,
}
impl Default for TemplateLimits {
fn default() -> Self {
Self {
count: 255,
max_timeout: 10,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct OrdinaryApiLimits {
pub app_domains: Vec<String>,
pub privileged_domains: Vec<String>,
pub max_default_timeout: u16,
pub proxy: ProxyLimits,
pub action: ActionLimits,
pub auth: AuthLimits,
pub integration: IntegrationLimits,
pub monitor: MonitorLimits,
pub storage: StorageLimits,
pub template: TemplateLimits,
}
impl Default for OrdinaryApiLimits {
fn default() -> Self {
Self {
app_domains: vec![],
privileged_domains: vec![],
max_default_timeout: 10,
proxy: ProxyLimits::default(),
action: ActionLimits::default(),
auth: AuthLimits::default(),
integration: IntegrationLimits::default(),
monitor: MonitorLimits::default(),
storage: StorageLimits::default(),
template: TemplateLimits::default(),
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ClientLoggingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
min_delay: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
max_delay: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
max_buffer: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
max_batch: Option<u16>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RedactedHashAlg {
Blake2,
Blake3,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ServerLoggingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub ips: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub headers: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub credentials: Option<RedactedHashAlg>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub sizes: Option<bool>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LoggingConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub client: Option<ClientLoggingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub server: Option<ServerLoggingConfig>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Check {
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TokenAlgorithm {
HmacBlake2b256,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct RefreshTokenConfig {
pub algorithm: TokenAlgorithm,
pub lifetime: u32,
pub rotation: u32,
}
impl Default for RefreshTokenConfig {
fn default() -> RefreshTokenConfig {
RefreshTokenConfig {
algorithm: TokenAlgorithm::HmacBlake2b256,
lifetime: 60 * 60 * 24 * 7,
rotation: 60 * 60 * 24 * 7 * 2,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AccessTokenConfig {
pub algorithm: TokenAlgorithm,
pub lifetime: u32,
pub rotation: u32,
pub claims: Vec<Field>,
}
impl Default for AccessTokenConfig {
fn default() -> AccessTokenConfig {
AccessTokenConfig {
algorithm: TokenAlgorithm::HmacBlake2b256,
lifetime: 60 * 60 * 24,
rotation: 60 * 60 * 24 * 3,
claims: vec![],
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ClientPasswordHash {
Sha256,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum PasswordProtocol {
Opaque,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct PasswordConfig {
pub protocol: PasswordProtocol,
}
impl Default for PasswordConfig {
fn default() -> PasswordConfig {
PasswordConfig {
protocol: PasswordProtocol::Opaque,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TotpAlgorithm {
Sha1,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TotpConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub template: Option<String>,
pub algorithm: TotpAlgorithm,
}
impl Default for TotpConfig {
fn default() -> TotpConfig {
TotpConfig {
template: None,
algorithm: TotpAlgorithm::Sha1,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct MfaConfig {
pub totp: TotpConfig,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum InviteMode {
Root,
Admin,
Viral,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct InviteConfig {
pub mode: InviteMode,
pub lifetime: u32,
pub clean_interval: (u32, u32),
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub claims: Option<Vec<Field>>,
}
impl Default for InviteConfig {
fn default() -> InviteConfig {
InviteConfig {
mode: InviteMode::Viral,
lifetime: 60 * 60 * 24 * 7,
clean_interval: (30, 90),
claims: None,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct AuthConfig {
pub password: PasswordConfig,
pub mfa: MfaConfig,
pub refresh_token: RefreshTokenConfig,
pub access_token: AccessTokenConfig,
pub cookies_enabled: bool,
pub client_hash: ClientPasswordHash,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub invite: Option<InviteConfig>,
}
impl Default for AuthConfig {
fn default() -> AuthConfig {
AuthConfig {
password: PasswordConfig::default(),
mfa: MfaConfig::default(),
refresh_token: RefreshTokenConfig::default(),
access_token: AccessTokenConfig::default(),
cookies_enabled: false,
client_hash: ClientPasswordHash::Sha256,
invite: None,
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub enum CompressionAlgorithm {
All,
Gzip,
Zstd { level: u8 },
Brotli,
Deflate,
}
impl CompressionAlgorithm {
#[must_use]
pub fn as_u8(&self) -> u8 {
match self {
Self::All => 0,
Self::Gzip => 1,
Self::Zstd { level: _ } => 2,
Self::Brotli => 3,
Self::Deflate => 4,
}
}
#[must_use]
pub fn as_char(&self) -> char {
match self {
Self::All => '0',
Self::Gzip => '1',
Self::Zstd { level: _ } => '2',
Self::Brotli => '3',
Self::Deflate => '4',
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::All => "",
Self::Gzip => "gzip",
Self::Zstd { level: _ } => "zstd",
Self::Brotli => "br",
Self::Deflate => "deflate",
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum StoredCachePolicy {
Permanent,
FRs(u64, u64),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct StoredCache {
pub policy: StoredCachePolicy,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub compression: Option<CompressionAlgorithms>,
#[serde(skip_serializing)]
#[serde(default)]
pub internal_compression: Option<ArrayVec<CompressionAlgorithm, 4>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_ttl: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub hit_ttl: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub frequency_window: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub clean_interval: Option<(u64, u64)>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub evict_on_dependency_change: Option<bool>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum XXH3Variation {
Bit64,
Bit128,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpEtagAlgorithm {
AHash,
XXH3(XXH3Variation),
Rustc,
Blake3,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HttpEtag {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub alg: Option<HttpEtagAlgorithm>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCacheControl {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_age: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub s_maxage: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub no_cache: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub no_store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub no_transform: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub must_revalidate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub proxy_revalidate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub private: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub public: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub immutable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stale_while_revalidate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stale_if_error: Option<bool>,
}
impl HttpCacheControl {
pub fn header_value(&self, cache_control: &mut String, default: &str) -> anyhow::Result<()> {
if let Some(max_age) = self.max_age {
write!(cache_control, "max-age={max_age}, ")?;
}
if let Some(s_maxage) = self.s_maxage {
write!(cache_control, "s-maxage={s_maxage}, ")?;
}
if let Some(no_cache) = self.no_cache
&& no_cache
{
write!(cache_control, "no-cache, ")?;
}
if let Some(no_store) = self.no_store
&& no_store
{
write!(cache_control, "no-store, ")?;
}
if let Some(no_transform) = self.no_transform
&& no_transform
{
write!(cache_control, "no-transform, ")?;
}
if let Some(must_revalidate) = self.must_revalidate
&& must_revalidate
{
write!(cache_control, "must-revalidate, ")?;
}
if let Some(proxy_revalidate) = self.proxy_revalidate
&& proxy_revalidate
{
write!(cache_control, "proxy-revalidate, ")?;
}
if let Some(stale_while_revalidate) = self.stale_while_revalidate
&& stale_while_revalidate
{
write!(cache_control, "stale-while-revalidate, ")?;
}
if let Some(private) = self.private
&& private
{
write!(cache_control, "private, ")?;
}
if let Some(public) = self.public
&& public
{
write!(cache_control, "public, ")?;
}
if let Some(immutable) = self.immutable
&& immutable
{
write!(cache_control, "immutable, ")?;
}
if let Some(stale_if_error) = self.stale_if_error
&& stale_if_error
{
write!(cache_control, "stale-if-error, ")?;
}
if cache_control.is_empty() {
cache_control.push_str(default);
} else {
cache_control.pop();
cache_control.pop();
}
Ok(())
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCache {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cache_control: Option<HttpCacheControl>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub expires: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub etag: Option<HttpEtag>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateField {
pub name: String,
pub kind: Kind,
pub value: serde_json::Value,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum QueryExpression {
Gte,
Gt,
Lte,
Lt,
Eq,
BeginsWith,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TemplateRefFieldBind {
Token {
field: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
expression: Option<QueryExpression>,
},
Segment {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
expression: Option<QueryExpression>,
},
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct TemplateRefField {
pub idx: u8,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub bind: Option<TemplateRefFieldBind>,
#[cfg_attr(feature = "utoipa", schema(no_recursion))]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub fields: Option<Vec<TemplateRefField>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateRef {
pub idx: u8,
pub name: String,
pub fields: Vec<TemplateRefField>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub all: Option<String>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateFlagRef {
pub idx: u8,
pub name: String,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateParamRef {
pub idx: u8,
pub name: String,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TemplateCache {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stored: Option<StoredCache>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub http: Option<HttpCache>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum WasmOpt {
Size,
SizeAggressive,
Level0,
Level1,
Level2,
Level3,
Level4,
}
impl WasmOpt {
#[must_use]
pub fn as_flag(&self) -> &'static str {
match self {
Self::Size => "-Os",
Self::SizeAggressive => "-Oz",
Self::Level0 => "-O0",
Self::Level1 => "-O1",
Self::Level2 => "-O2",
Self::Level3 => "-O3",
Self::Level4 => "-O4",
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for TemplateFfiVersion {
fn default() -> Self {
Self::V1
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TemplateFfiVersion {
V1,
}
#[allow(clippy::derivable_impls)]
impl Default for TemplateFfiSerialization {
fn default() -> Self {
Self::FlexBufferVector
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum TemplateFfiSerialization {
FlexBufferVector,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct TemplateFfi {
pub version: TemplateFfiVersion,
pub serialization: TemplateFfiSerialization,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct TemplateConfig {
pub ffi: TemplateFfi,
pub idx: u8,
pub name: String,
pub mime: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub path: Option<String>,
pub route: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub protected: Option<Check>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cache: Option<TemplateCache>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub csp: Option<HttpCsp>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cors: Option<HttpCors>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timeout: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub fields: Option<Vec<TemplateField>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub globals: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub flags: Option<Vec<TemplateFlagRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub params: Option<Vec<TemplateParamRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub models: Option<Vec<TemplateRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub content: Option<Vec<TemplateRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub actions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub wasm_opt: Option<WasmOpt>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub variables: Option<Vec<String>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct CompressionAlgorithms(pub Vec<CompressionAlgorithm>);
impl CompressionAlgorithms {
#[must_use]
fn get_list(&self) -> ArrayVec<CompressionAlgorithm, 4> {
let mut list = ArrayVec::<CompressionAlgorithm, 4>::new();
let mut has_all = false;
for alg in &self.0 {
if *alg == CompressionAlgorithm::All {
has_all = true;
} else if !list.contains(alg) {
list.push(alg.clone());
}
}
if has_all {
for alg in [
CompressionAlgorithm::Brotli,
CompressionAlgorithm::Zstd { level: 17 },
CompressionAlgorithm::Deflate,
CompressionAlgorithm::Gzip,
] {
if !list.contains(&alg) {
list.push(alg);
}
}
}
list
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AssetsConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub dir_path: Option<String>,
#[serde(default = "AssetsConfig::default_base_route")]
pub base_route: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub append_index_html: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub skip_base_route_index_html: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub append_html_ext: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub preserve_exif: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub html_csp: Option<HttpCsp>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub http: Option<HttpCache>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub precompression: Option<CompressionAlgorithms>,
#[serde(skip_serializing)]
#[serde(default)]
pub internal_precompression: Option<ArrayVec<CompressionAlgorithm, 4>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify_css: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify_js: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minify_html: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub internal_cache_control_header_value: Option<String>,
}
impl AssetsConfig {
fn default_base_route() -> String {
"/assets".to_string()
}
}
impl AssetsConfig {
pub fn init(&mut self, default_cache_control_header_value: &str) -> anyhow::Result<()> {
if let Some(http_cache) = &self.http
&& let Some(http_cache_control) = &http_cache.cache_control
{
let mut header = String::new();
http_cache_control.header_value(&mut header, default_cache_control_header_value)?;
self.internal_cache_control_header_value = Some(header);
}
Ok(())
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FragmentsConfig {
pub dir_path: String,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Global {
pub name: String,
pub kind: Kind,
pub value: serde_json::Value,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SecretSource {
Env,
Stored,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SecretVisibility {
Integrations,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Secret {
pub name: String,
pub source: SecretSource,
pub visibility: SecretVisibility,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FlagOption {
pub idx: u8,
pub name: String,
pub percentage: u8,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Flag {
pub idx: u8,
pub name: String,
pub options: Vec<FlagOption>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentDefinition {
pub idx: u8,
pub name: String,
pub fields: Vec<Field>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub lifecycle: Option<ContentObjectLifecycle>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentObjectLifecycle {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub before_all: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub on_add: Option<LifecycleBeforeAfterScripts>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub on_edit: Option<LifecycleBeforeAfterScripts>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub on_delete: Option<LifecycleBeforeAfterScripts>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Content {
pub file_path: String,
pub definitions: Vec<ContentDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub update: Option<ContentUpdateConfig>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentUpdateConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub lifecycle: Option<LifecycleBeforeAfterScripts>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum UuidVersion {
V4,
V7,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ModelConfig {
pub idx: u8,
pub name: String,
pub fields: Vec<Field>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub uuid: Option<UuidVersion>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IntegrationProtocolHttpEncoding {
Json,
Text,
None,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IntegrationProtocol {
Http {
method: String,
headers: Vec<(String, String)>,
send_encoding: IntegrationProtocolHttpEncoding,
recv_encoding: IntegrationProtocolHttpEncoding,
},
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IntegrationConfig {
pub idx: u8,
pub name: String,
pub protocol: IntegrationProtocol,
pub endpoint: String,
pub send: Kind,
pub recv: Kind,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub secrets: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timeout: Option<u16>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionLang {
Rust,
JavaScript,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessModelOps {
Insert,
Get,
Query,
Search,
Update,
Delete,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessAuthOps {
SetTokenFields,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionAccessPermission {
Model {
name: String,
ops: Vec<ActionAccessModelOps>,
},
Content {
name: String,
},
Integration {
name: String,
},
Action {
name: String,
},
Auth {
ops: Vec<ActionAccessAuthOps>,
},
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpMethod {
PUT,
POST,
GET,
DELETE,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionTriggerModelOps {
Insert,
Update,
Delete,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionTrigger {
Ordinary,
Json {
route: String,
method: HttpMethod,
},
Form {
route: String,
method: HttpMethod,
redirect: String,
},
Login,
Registration,
Content { name: String },
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionFfiVersion {
V1,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ActionFfiSerialization {
FlexBufferVector,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionFfi {
pub version: ActionFfiVersion,
pub serialization: ActionFfiSerialization,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ActionConfig {
pub ffi: ActionFfi,
pub idx: u8,
pub name: String,
pub lang: ActionLang,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub dir_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub protected: Option<Check>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub transactional: Option<bool>,
pub access: Vec<ActionAccessPermission>,
pub accepts: Kind,
pub returns: Kind,
pub triggered_by: Vec<ActionTrigger>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timeout: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cors: Option<HttpCors>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub wasm_opt: Option<WasmOpt>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub privileged: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub variables: Option<Vec<String>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct ErrorConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub asset: Option<String>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RuntimeMode {
Shared,
SingleThreaded,
MultiThreaded,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowHeaders {
Any,
Headers(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsExposeHeaders {
Any,
Headers(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowMethods {
Any,
Methods(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum HttpCorsAllowOrigin {
Any,
Origins(Vec<String>),
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCors {
pub allow_credentials: Option<bool>,
pub allow_headers: Option<HttpCorsAllowHeaders>,
pub max_age: Option<u32>,
pub allow_methods: Option<HttpCorsAllowMethods>,
pub allow_origin: Option<HttpCorsAllowOrigin>,
pub expose_headers: Option<HttpCorsExposeHeaders>,
pub allow_private_network: Option<bool>,
}
impl HttpCors {
#[must_use]
pub fn overwrite(&self, base: &Self) -> Self {
Self {
allow_credentials: if self.allow_credentials.is_none() {
base.allow_credentials
} else {
self.allow_credentials
},
allow_headers: if self.allow_headers.is_none() {
base.allow_headers.clone()
} else {
self.allow_headers.clone()
},
max_age: if self.max_age.is_none() {
base.max_age
} else {
self.max_age
},
allow_methods: if self.allow_methods.is_none() {
base.allow_methods.clone()
} else {
self.allow_methods.clone()
},
allow_origin: if self.allow_origin.is_none() {
base.allow_origin.clone()
} else {
self.allow_origin.clone()
},
expose_headers: if self.expose_headers.is_none() {
base.expose_headers.clone()
} else {
self.expose_headers.clone()
},
allow_private_network: if self.allow_private_network.is_none() {
base.allow_private_network
} else {
self.allow_private_network
},
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct HttpCsp {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub default_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub script_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub style_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub font_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub img_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub frame_src: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub include_inline_hashes: Option<bool>,
}
impl HttpCsp {
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn build_string(
&self,
base: &Self,
inline_style_hashes: Option<Vec<String>>,
inline_script_hashes: Option<Vec<String>>,
script_urls: Option<Vec<String>>,
secure: bool,
has_wasm: bool,
) -> String {
let mut out = String::new();
let include_inline_hashes = self
.include_inline_hashes
.unwrap_or(base.include_inline_hashes.unwrap_or(true));
let default_src = self
.default_src
.clone()
.unwrap_or(base.default_src.clone().unwrap_or("'self'".to_string()));
if !default_src.is_empty() {
out.push_str("default-src ");
out.push_str(default_src.as_str());
out.push_str("; ");
}
let mut script_src = self
.script_src
.clone()
.unwrap_or(base.script_src.clone().unwrap_or_default());
if include_inline_hashes
&& let Some(script_hashes) = inline_script_hashes
&& !script_hashes.is_empty()
{
if script_src.is_empty() {
script_src.push_str("'self'");
if has_wasm {
script_src.push_str(" 'wasm-unsafe-eval'");
}
}
for hash in script_hashes {
script_src.push_str(" '");
script_src.push_str(hash.as_str());
script_src.push('\'');
}
}
if let Some(script_urls) = script_urls {
if script_src.is_empty() {
script_src.push_str("'self'");
}
for script_url in script_urls {
script_src.push(' ');
script_src.push_str(&script_url);
}
}
if !script_src.is_empty() {
out.push_str("script-src ");
out.push_str(script_src.as_str());
out.push_str("; ");
}
let mut style_src = self
.style_src
.clone()
.unwrap_or(base.style_src.clone().unwrap_or_default());
if include_inline_hashes
&& let Some(style_hashes) = inline_style_hashes
&& !style_hashes.is_empty()
{
if style_src.is_empty() {
style_src.push_str("'self'");
}
for hash in style_hashes {
style_src.push_str(" '");
style_src.push_str(hash.as_str());
style_src.push('\'');
}
}
if !style_src.is_empty() {
out.push_str("style-src ");
out.push_str(style_src.as_str());
out.push_str("; ");
}
let font_src = self
.font_src
.clone()
.unwrap_or(base.font_src.clone().unwrap_or_default());
if !font_src.is_empty() {
out.push_str("font-src ");
out.push_str(font_src.as_str());
out.push_str("; ");
}
let img_src = self
.img_src
.clone()
.unwrap_or(base.img_src.clone().unwrap_or_default());
if !img_src.is_empty() {
out.push_str("img-src ");
out.push_str(img_src.as_str());
out.push_str("; ");
}
let frame_src = self
.frame_src
.clone()
.unwrap_or(base.frame_src.clone().unwrap_or_default());
if !frame_src.is_empty() {
out.push_str("frame-src ");
out.push_str(frame_src.as_str());
out.push_str("; ");
}
if secure {
out.push_str("upgrade-insecure-requests; ");
}
out.push_str("report-to csp");
out = out.trim().to_string();
out
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LifecycleBeforeAfterScripts {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub before: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub after: Option<Vec<Vec<String>>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TopLevelLifecycle {
pub before_all: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub build: Option<LifecycleBeforeAfterScripts>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum RedirectMethod {
Temporary,
Permanent,
}
impl Display for RedirectMethod {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Temporary => write!(f, "TEMPORARY"),
Self::Permanent => write!(f, "PERMANENT"),
}
}
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct HostRedirect {
pub from: String,
pub to: String,
pub method: RedirectMethod,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct RouteRedirect {
pub condition: String,
pub rule: (String, String),
pub method: RedirectMethod,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Redirects {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub host: Option<Vec<HostRedirect>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub route: Option<Vec<RouteRedirect>>,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProxyConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub port: Option<u16>,
pub target: String,
}
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "docs", derive(schemars::JsonSchema))]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct OrdinaryConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub lifecycle: Option<TopLevelLifecycle>,
pub domain: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cnames: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub canonical: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub redirects: Option<Redirects>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub proxies: Option<Vec<ProxyConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub contacts: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub hide_contacts: Option<bool>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default = "OrdinaryConfig::default_storage_size")]
pub storage_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub default_timeout: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub csp: Option<HttpCsp>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub cors: Option<HttpCors>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub runtime: Option<RuntimeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub hide_schema: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub client_rendering: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub obfuscation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub client_events: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub redirect_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub logging: Option<LoggingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub error: Option<ErrorConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub auth: Option<AuthConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub globals: Option<Vec<Global>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub secrets: Option<Vec<Secret>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub flags: Option<Vec<Flag>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub content: Option<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub models: Option<Vec<ModelConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub integrations: Option<Vec<IntegrationConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub actions: Option<Vec<ActionConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub assets: Option<AssetsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub fragments: Option<FragmentsConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub templates: Option<Vec<TemplateConfig>>,
}
impl OrdinaryConfig {
pub fn get(proj_path: &str) -> anyhow::Result<OrdinaryConfig> {
let path = Path::new(proj_path).join("ordinary.json");
let config_json = fs_err::read_to_string(&path)?;
let mut config = match serde_json::from_str::<OrdinaryConfig>(config_json.as_str()) {
Ok(config) => config,
Err(err) => bail!("{}: {err}", path.display()),
};
config.load_internal_compression();
Ok(config)
}
pub fn load_internal_compression(&mut self) {
if let Some(assets) = self.assets.as_mut()
&& let Some(precompression) = &assets.precompression
{
assets.internal_precompression = Some(precompression.get_list());
}
if let Some(templates) = self.templates.as_mut() {
for template in templates {
if let Some(cache) = template.cache.as_mut()
&& let Some(stored) = cache.stored.as_mut()
&& let Some(compression) = &stored.compression
{
stored.internal_compression = Some(compression.get_list());
}
}
}
}
pub fn for_send(&self) -> anyhow::Result<OrdinaryConfig> {
let mut config = self.clone();
config.lifecycle = None;
if let Some(assets) = config.assets.as_mut() {
assets.dir_path = None;
}
if let Some(content) = config.content.as_mut() {
content.update = None;
for def in &mut content.definitions {
def.lifecycle = None;
}
}
if let Some(templates) = config.templates.as_mut() {
for template in templates {
template.path = None;
template.wasm_opt = None;
template.minify = None;
}
}
if let Some(actions) = config.actions.as_mut() {
for action in actions {
action.wasm_opt = None;
action.dir_path = None;
}
}
Ok(config)
}
#[instrument(skip_all, err, level = "debug")]
pub fn validate(&self) -> anyhow::Result<()> {
validate(self)
}
#[must_use]
#[allow(clippy::unnecessary_wraps)]
pub fn default_storage_size() -> Option<u64> {
Some(5_000_000)
}
#[must_use]
pub fn has_ordinary_actions(&self) -> bool {
if let Some(actions) = &self.actions {
actions
.iter()
.find(|a| {
for trigger in &a.triggered_by {
if let ActionTrigger::Ordinary = trigger {
return true;
}
}
false
})
.is_some()
} else {
false
}
}
#[allow(clippy::too_many_lines)]
pub fn check_config_against_limits(
&self,
limits: &OrdinaryApiLimits,
privileged_domains: &HashSet<String>,
) -> anyhow::Result<()> {
if privileged_domains.contains(&self.domain) {
return Ok(());
}
if let Some(canonical) = &self.canonical {
let mut is_valid = false;
if canonical == &self.domain {
is_valid = true;
}
if let Some(cnames) = &self.cnames {
for cname in cnames {
if cname == canonical {
is_valid = true;
break;
}
}
}
if !is_valid {
bail!("canonical: {canonical}, is not in 'domain' or 'cnames'");
}
}
if let Some(default_timeout) = self.default_timeout
&& default_timeout > limits.max_default_timeout
{
bail!(
"default timeout {} greater than limit {}",
default_timeout,
limits.max_default_timeout
);
}
if let Some(proxies) = &self.proxies {
if proxies.len() > limits.proxy.count as usize {
bail!(
"proxy count {} greater than limit {}",
proxies.len(),
limits.proxy.count
);
}
let mut disallowed_target_regexes =
Vec::with_capacity(limits.proxy.disallowed_targets.len());
for disallowed in &limits.proxy.disallowed_targets {
disallowed_target_regexes.push(Regex::new(disallowed)?);
}
for proxy in proxies {
for re in &disallowed_target_regexes {
if re.is_match(&proxy.target) {
bail!(
"proxy target {} is disallowed by rule {}",
proxy.target,
re.as_str()
)
}
}
}
}
if let Some(templates) = &self.templates {
if templates.len() > limits.template.count as usize {
bail!(
"template count {} greater than limit {}",
templates.len(),
limits.template.count
);
}
for template in templates {
if let Some(timeout) = template.timeout
&& timeout > limits.template.max_timeout
{
bail!(
"template timeout {} greater than limit {} for {}",
timeout,
limits.template.max_timeout,
template.name
);
}
if let Some(cache) = &template.cache
&& let Some(stored_cache) = &cache.stored
{
if let Some(max_size) = stored_cache.max_size
&& (max_size < limits.storage.cache.max_size_range.0
|| max_size > limits.storage.cache.max_size_range.1)
{
bail!(
"template cache max_size {} is not within range [{}, {}] for {}",
max_size,
limits.storage.cache.max_size_range.0,
limits.storage.cache.max_size_range.1,
template.name
);
}
if let Some(max_count) = stored_cache.max_count
&& (max_count < limits.storage.cache.max_count_range.0
|| max_count > limits.storage.cache.max_count_range.1)
{
bail!(
"template cache max_count {} is not within range [{}, {}] for {}",
max_count,
limits.storage.cache.max_count_range.0,
limits.storage.cache.max_count_range.1,
template.name
);
}
if let Some((clean_interval_min, clean_interval_max)) =
stored_cache.clean_interval
{
if clean_interval_min < limits.storage.cache.clean_interval_ranges.0.0
|| clean_interval_min > limits.storage.cache.clean_interval_ranges.0.1
{
bail!(
"template cache clean_interval min {} is not within range [{}, {}] for {}",
clean_interval_min,
limits.storage.cache.clean_interval_ranges.0.0,
limits.storage.cache.clean_interval_ranges.0.1,
template.name
);
}
if clean_interval_max < limits.storage.cache.clean_interval_ranges.1.0
|| clean_interval_max > limits.storage.cache.clean_interval_ranges.1.1
{
bail!(
"template cache clean_interval min {} is not within range [{}, {}] for {}",
clean_interval_max,
limits.storage.cache.clean_interval_ranges.1.0,
limits.storage.cache.clean_interval_ranges.1.1,
template.name
);
}
}
}
}
}
if let Some(integrations) = &self.integrations {
if integrations.len() > limits.integration.count as usize {
bail!(
"integration count {} greater than limit {}",
integrations.len(),
limits.integration.count
);
}
for integration in integrations {
if let Some(timeout) = integration.timeout
&& timeout > limits.integration.max_timeout
{
bail!(
"integration timeout {} greater than limit {} for {}",
timeout,
limits.integration.max_timeout,
integration.name,
);
}
}
}
if let Some(actions) = &self.actions {
if actions.len() > limits.action.count as usize {
bail!(
"action count {} greater than limit {}",
actions.len(),
limits.action.count
);
}
for action in actions {
if action.privileged == Some(true) {
bail!("action {} is not under a privileged domain", action.name);
}
if let Some(timeout) = action.timeout
&& timeout > limits.action.max_timeout
{
bail!(
"action timeout {} greater than limit {} for {}",
timeout,
limits.action.max_timeout,
action.name
);
}
}
}
if let Some(storage_size) = self.storage_size
&& storage_size > limits.storage.max_app_storage
{
bail!("storage size is greater than limit");
}
if let Some(content) = &self.content {
if content.definitions.len() > limits.storage.content.max_content_definitions as usize {
bail!(
"content definition count {} greater than limit {}",
content.definitions.len(),
limits.storage.content.max_content_definitions
);
}
for content_def in &content.definitions {
if content_def.fields.len() > limits.storage.content.max_content_fields as usize {
bail!(
"content field count {} greater than limit {} for {}",
content_def.fields.len(),
limits.storage.content.max_content_fields,
content_def.name,
);
}
for field in &content_def.fields {
if field.searchable == Some(true) && !limits.storage.content.search_enabled {
bail!(
"content field cannot be 'searchable' for field {} on definition {}",
field.name,
content_def.name,
);
}
}
}
}
if let Some(models) = &self.models {
if models.len() > limits.storage.model.max_model_definitions as usize {
bail!(
"model definition count {} greater than limit {}",
models.len(),
limits.storage.model.max_model_definitions
);
}
for model in models {
if model.fields.len() > limits.storage.model.max_model_fields as usize {
bail!(
"model field count {} greater than limit {} for {}",
model.fields.len(),
limits.storage.content.max_content_fields,
model.name,
);
}
for field in &model.fields {
if field.searchable == Some(true) && !limits.storage.content.search_enabled {
bail!(
"content field cannot be 'searchable' for field {} on definition {}",
field.name,
model.name,
);
}
}
}
}
if let Some(secrets) = &self.secrets
&& secrets.len() > limits.storage.secrets.max_count as usize
{
bail!(
"secrets len {} exceeds max count limit {}",
secrets.len(),
limits.storage.secrets.max_count
);
}
Ok(())
}
pub fn exec_lifecycle_script(
proj_path: &Path,
argument: &Option<String>,
name: &str,
when: &str,
scripts: &Vec<Vec<String>>,
) -> anyhow::Result<()> {
let span = tracing::info_span!("lifecycle", %when, %name);
span.in_scope(|| {
let curr_dir = env::current_dir()?;
env::set_current_dir(proj_path)?;
for script in scripts {
let mut script_iter = script.iter();
if let Some(command) = script_iter.nth(0) {
let mut command_str = command.clone();
let mut command = Command::new(command);
for arg in script_iter {
write!(command_str, " {arg}")?;
command.arg(arg);
}
tracing::info!(cmd = %command_str, "exec");
let output = match &argument {
Some(arg) => command.arg(arg).output()?,
None => command.output()?,
};
if output.status.success() {
tracing::info!("success");
} else {
let stderr = str::from_utf8(&output.stderr)?;
let stdout = str::from_utf8(&output.stdout)?;
tracing::error!(%stderr, %stdout, "failed");
bail!(stderr.to_string());
}
}
}
env::set_current_dir(curr_dir)?;
anyhow::Ok(())
})
}
}