use crate::{ProvisionMode, traverse};
use anyhow::bail;
use getrandom::SysRng;
use getrandom::rand_core::Rng;
use hashbrown::HashMap;
use ordinary_app::server::OrdinaryAppServer;
use ordinary_auth::Auth;
use ordinary_config::{OrdinaryApiLimits, OrdinaryConfig};
use ordinary_storage::saferlmdb::EnvBuilder;
use ordinary_storage::{Storage, saferlmdb};
use ordinary_utils::{SecurityMode, shutdown_signal};
use rand_chacha::rand_core::SeedableRng;
use std::fs::File;
use std::io::Write;
use std::net::Ipv6Addr;
use std::path::Path;
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
#[allow(
clippy::fn_params_excessive_bools,
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::missing_panics_doc
)]
pub async fn run(
project_dir: &str,
domain_override: &Option<String>,
data_dir: &str,
log_size: bool,
insecure: bool,
insecure_cookies: bool,
log_headers: bool,
log_ips: bool,
port: Option<u16>,
redirect_port: Option<u16>,
provision: &ProvisionMode,
danger_dns_no_verify: bool,
) -> anyhow::Result<()> {
tracing::debug!("installing standalone project...");
let project_path = Path::new(&project_dir);
tracing::debug!("reading config...");
let mut config = OrdinaryConfig::get(project_dir)?;
config.validate()?;
if !danger_dns_no_verify {
OrdinaryAppServer::verify_custom_domains(&config).await?;
}
if let Some(domain_override) = &domain_override {
config.domain = domain_override.clone();
}
let data_dir = Path::new(data_dir).join("apps").join(&config.domain);
tracing::debug!("setting up DB environment...");
let store_path = data_dir.join("store");
std::fs::create_dir_all(&store_path)?;
let ps = page_size::get() as u64;
let storage_size = match &config.storage_size {
Some(s) => *s,
None => OrdinaryConfig::default_storage_size().unwrap_or_default(),
};
let remainder = storage_size % ps;
let mapsize = (storage_size - remainder) + ps;
tracing::info!(mapsize = %bytesize::ByteSize(mapsize).display().si_short());
let env = Arc::new(unsafe {
let mut env_builder = EnvBuilder::new()?;
env_builder.set_maxreaders(126)?;
env_builder.set_mapsize(usize::try_from(mapsize)?)?;
env_builder.set_maxdbs(13)?;
env_builder.open(
store_path.to_str().expect("store_path not a str"),
&saferlmdb::open::Flags::empty(),
0o600,
)?
});
tracing::debug!("setting up auth...");
let keys_dir = data_dir.join("keys");
std::fs::create_dir_all(&keys_dir)?;
let auth_key_path = keys_dir.join("auth");
let auth_key: [u8; 32] = if auth_key_path.exists() && auth_key_path.is_file() {
let auth_key = std::fs::read(&auth_key_path)?;
let auth_key: [u8; 32] = auth_key[..].try_into()?;
auth_key
} else {
let mut auth_key = [0u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::try_from_rng(&mut SysRng)?;
rng.fill_bytes(&mut auth_key[..]);
let mut auth_key_file = File::create(auth_key_path)?;
auth_key_file.write_all(&auth_key)?;
auth_key_file.flush()?;
auth_key
};
tracing::debug!("initializing auth...");
let auth = Arc::new(Auth::new(
config.domain.clone(),
config.auth.clone(),
auth_key,
env.clone(),
)?);
tracing::debug!("initializing storage...");
let storage_key_path = keys_dir.join("storage");
let storage_key: [u8; 32] = if storage_key_path.exists() && storage_key_path.is_file() {
let storage_key = std::fs::read(&storage_key_path).expect("failed to read storage key");
let storage_key: [u8; 32] = storage_key[..]
.try_into()
.expect("failed to get fixed storage key");
storage_key
} else {
let mut storage_key = [0u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::try_from_rng(&mut SysRng)?;
rng.fill_bytes(&mut storage_key[..]);
let mut storage_key_file =
File::create(storage_key_path).expect("failed to create storage key file");
storage_key_file
.write_all(&storage_key)
.expect("failed to write storage key");
storage_key_file
.flush()
.expect("failed to flush storage key");
storage_key
};
let limits = OrdinaryApiLimits::default();
let storage = Arc::new(Storage::new(
limits.storage.clone(),
if let Some(models) = &config.models {
models.clone()
} else {
vec![]
},
if let Some(content) = &config.content {
content.definitions.clone()
} else {
vec![]
},
storage_key,
&env,
store_path.join("search"),
log_size,
)?);
tracing::debug!("instantiating app...");
let app = Arc::new(
OrdinaryAppServer::new(
config.for_send()?,
limits,
auth,
storage,
!insecure,
!insecure_cookies,
log_headers,
log_ips,
None,
None,
None,
None,
)
.await?,
);
let app_clone = app.clone();
tracing::debug!("copying content...");
if let Some(content) = config.content {
let content_path = Path::new(&content.file_path);
let objects_json = std::fs::read_to_string(project_path.join(content_path))?;
let objects: Vec<ordinary_types::ContentObject> = serde_json::from_str(&objects_json)?;
app.update_content(&objects).await?;
}
tracing::debug!("copying assets...");
if let Some(assets) = config.assets {
let Some(assets_dir_path) = &assets.dir_path else {
bail!("assets.dir_path cannot be unset");
};
let parent_path = &project_path.join(assets_dir_path);
let assets_path_content = Arc::new(Mutex::new(Vec::new()));
traverse(parent_path, &|entry| {
let path = entry.path();
if path.ends_with(".DS_Store") {
tracing::warn!("ignoring .DS_Store");
} else if let Ok(content) = std::fs::read(&path)
&& let Ok(path) = path.strip_prefix(parent_path)
&& let Some(path) = path.to_str()
&& let Ok(lock) = assets_path_content.lock()
{
let mut lock = lock;
lock.push((path.to_string(), content));
}
})?;
let mut assets = vec![];
if let Ok(lock) = assets_path_content.lock() {
for (path, content) in lock.iter() {
assets.push((path.clone(), content.clone()));
}
}
for (path, content) in &assets {
app_clone.put_asset(path, content).await?;
}
}
if config.auth.is_some() {
let core_js =
std::fs::read_to_string(project_path.join("./.ordinary/gen/client/js/core.js"))?;
app.put_asset(
&format!("{}/js/core.js", config.version),
core_js.replace("{{ version }}", &config.version).as_bytes(),
)
.await?;
}
if config.auth.is_some() {
let js_client_js =
std::fs::read_to_string(project_path.join("./.ordinary/gen/client/js/client.js"))?;
app.put_asset(
&format!("{}/js/client.js", config.version),
js_client_js
.replace("{{ version }}", &config.version)
.as_bytes(),
)
.await?;
}
if config.auth.is_some()
|| config.obfuscation == Some(true)
|| config.client_rendering == Some(true)
{
let client_js =
std::fs::read_to_string(project_path.join("./.ordinary/gen/client/wasm/client.js"))?;
let client_wasm = std::fs::read(
Path::new(project_path).join(".ordinary/gen/client/wasm/client_bg_opt.wasm"),
)
.or_else(|_| {
std::fs::read(Path::new(project_path).join(".ordinary/gen/client/wasm/client_bg.wasm"))
})?;
app.put_asset(
&format!("{}/wasm/client.js", config.version),
client_js
.replace("{{ version }}", &config.version)
.as_bytes(),
)
.await?;
app.put_asset(
&format!("{}/wasm/client_bg.wasm", config.version),
&client_wasm,
)
.await?;
}
if let Some(actions) = config.actions {
if !actions.is_empty() {
tracing::debug!("installing actions...");
}
for config in actions {
let Some(dir_path) = &config.dir_path else {
tracing::error!("no dir_path provided for action");
bail!("no dir_path provided for action");
};
let path = project_path
.join(dir_path)
.join("target/wasm32-wasip1/release/action.wasm");
let action_bytes = std::fs::read(path)?;
app.set_action(config.idx, action_bytes.as_slice()).await?;
}
}
if let Some(templates) = config.templates {
if !templates.is_empty() {
tracing::debug!("installing templates...");
}
for config in templates {
let path = project_path
.join(".ordinary")
.join("gen")
.join("server")
.join(&config.name)
.join("target/wasm32-wasip1/release/template.wasm");
let template_bytes = std::fs::read(path)?;
let path = project_path
.join(".ordinary")
.join("gen")
.join("hashes")
.join(&config.name);
let style_path = path.join("style-src.json");
let script_path = path.join("script-src.json");
let csp_style_hashes = if style_path.exists() {
let csp_style_hashes: Vec<String> =
serde_json::from_str(&std::fs::read_to_string(style_path)?)?;
Some(csp_style_hashes)
} else {
None
};
let csp_script_hashes = if script_path.exists() {
let csp_script_hashes: Vec<String> =
serde_json::from_str(&std::fs::read_to_string(script_path)?)?;
Some(csp_script_hashes)
} else {
None
};
app.set_template(
config.idx,
template_bytes.as_slice(),
csp_style_hashes,
csp_script_hashes,
!insecure,
)
.await?;
}
}
let port = if insecure {
port.unwrap_or(config.port.unwrap_or(80))
} else {
port.unwrap_or(config.port.unwrap_or(443))
};
let mut proxy_listeners = HashMap::new();
for (domain, (_, proxy_port)) in &*app.proxies {
if let Some(proxy_port) = proxy_port {
let listener = TcpListener::bind((Ipv6Addr::UNSPECIFIED, *proxy_port)).await?;
proxy_listeners.insert(domain.clone(), listener);
} else {
let listener = TcpListener::bind((Ipv6Addr::UNSPECIFIED, 0)).await?;
proxy_listeners.insert(domain.clone(), listener);
}
}
let redirect_listener = if insecure {
None
} else {
let redirect_port = redirect_port.unwrap_or(config.redirect_port.unwrap_or(80));
Some(TcpListener::bind((Ipv6Addr::UNSPECIFIED, redirect_port)).await?)
};
let listener = TcpListener::bind((Ipv6Addr::UNSPECIFIED, port)).await?;
app.start(
listener,
redirect_listener,
Some(proxy_listeners),
if insecure {
SecurityMode::Insecure
} else {
SecurityMode::Secure(
Path::new(&data_dir).join(&config.domain).join("certs"),
match provision {
ProvisionMode::Localhost => ordinary_utils::ProvisionMode::Localhost,
ProvisionMode::Staging => ordinary_utils::ProvisionMode::Staging,
ProvisionMode::Production => ordinary_utils::ProvisionMode::Production,
},
)
},
None,
shutdown_signal,
)
.await
}