pub(crate) mod apps;
pub(crate) mod auth;
pub(crate) mod openapi;
pub(crate) mod ops;
mod start;
use hashbrown::{HashMap, HashSet};
use ordinary_app::server::OrdinaryAppServer;
#[cfg(feature = "server")]
use ordinary_config::OrdinaryApiLimits;
use ordinary_monitor::service::OrdinaryMonitorService;
use ordinary_storage::Storage;
use parking_lot::{Mutex, RwLock};
use rand_chacha::rand_core::Rng;
use rand_chacha::rand_core::SeedableRng;
use saferlmdb::{EnvBuilder, Environment};
use sha2::{Digest, Sha256};
use std::io::Write;
use std::process;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio_rustls::StartHandshake;
use axum::Router;
use std::net::SocketAddr;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tokio_rustls::rustls::ServerConfig;
use tracing::{Instrument, Span};
use ordinary_auth::{Auth, AuthClient};
use std::fs;
use crate::server::auth::AccountLockManager;
use crate::server::start::start;
use crate::{api_account_claims, api_invite_claims};
use anyhow::bail;
use getrandom::SysRng;
use ordinary_config::{
AccessTokenConfig, AuthConfig, InviteConfig, InviteMode, MfaConfig, OrdinaryApiConfig,
PasswordConfig, RedactedHashAlg, RefreshTokenConfig, TotpConfig,
};
use ordinary_monitor::tracing::logger::OrdinaryLogger;
use ordinary_utils::{ProvisionMode, SecurityMode};
use sysinfo::{Pid, System};
use tokio::sync::watch::{Receiver, Sender};
use x25519_dalek::{PublicKey, StaticSecret};
pub struct WrappedOrdinaryAppServer {
port: u16,
app: Arc<OrdinaryAppServer>,
terminate_tx: Sender<bool>,
stream_tx: tokio::sync::mpsc::UnboundedSender<(StartHandshake<TcpStream>, SocketAddr, Span)>,
dh_keypair: (StaticSecret, PublicKey),
}
pub struct WrappedOrdinaryProxyServer {
service: Router,
terminate_rx: Receiver<bool>,
configs: Option<(Arc<ServerConfig>, Arc<ServerConfig>)>,
}
type Dbs = Arc<tokio::sync::Mutex<HashMap<String, (Arc<Environment>, Arc<Auth>, Arc<Storage>)>>>;
pub enum Server {
App(Arc<WrappedOrdinaryAppServer>),
Proxy(Arc<WrappedOrdinaryProxyServer>),
}
type AppServers = Arc<tokio::sync::RwLock<HashMap<String, Server>>>;
#[allow(clippy::struct_excessive_bools)]
pub struct OrdinaryApiServerState {
pub domain: String,
pub app_domains: Arc<Vec<String>>,
pub secure: bool,
pub secure_cookies: bool,
pub log_headers: bool,
pub log_ips: bool,
pub log_size: bool,
pub auth: Arc<Auth>,
pub servers: AppServers,
pub apps_dir: PathBuf,
pub dbs: Dbs,
pub env_name: String,
pub provision_mode: Option<ProvisionMode>,
pub dedicated_ports: bool,
pub server_span: Span,
pub monitor: Arc<Option<OrdinaryMonitorService>>,
pub stored_logs: bool,
pub limits: String,
pub config: OrdinaryApiConfig,
pub signal_tx: Arc<RwLock<Option<Sender<()>>>>,
pub close_rx: Arc<RwLock<Option<Receiver<()>>>>,
pub privileged_domains: HashSet<String>,
pub pid: Pid,
pub system: Arc<Mutex<System>>,
pub account_lock_manager: Arc<AccountLockManager>,
pub danger_dns_no_verify: bool,
}
pub struct OrdinaryApiServer {
auth: Arc<Auth>,
env: Arc<Environment>,
config: OrdinaryApiConfig,
servers: AppServers,
apps_dir: PathBuf,
monitor: Arc<Option<OrdinaryMonitorService>>,
}
#[cfg(feature = "server")]
impl OrdinaryApiServer {
#[allow(clippy::similar_names, clippy::too_many_arguments)]
pub async fn init(
env_name: &str,
domain: &str,
password: &str,
env_path: impl AsRef<Path>,
storage_size: usize,
api_contacts: &[String],
app_domains: &[String],
privileged_domains: &Option<Vec<String>>,
logger: Option<OrdinaryLogger>,
) -> anyhow::Result<(String, Vec<u8>)> {
let span = tracing::info_span!("init", env = %env_name, pid = process::id());
let mut limits = OrdinaryApiLimits::default();
if let Some(pd) = privileged_domains {
pd.clone_into(&mut limits.privileged_domains);
}
limits.app_domains = app_domains.to_vec();
async {
let config = OrdinaryApiConfig {
domain: domain.into(),
contacts: api_contacts.to_vec(),
public_dns_ip: None,
env_name: env_name.to_string(),
limits,
};
let config_file = serde_json::to_string_pretty(&config)?;
let data_path = env_path.as_ref().join("data");
if data_path.exists() {
bail!("environment already initialized");
}
fs::write(env_path.as_ref().join("ordinaryd.json"), config_file)?;
let api_server =
OrdinaryApiServer::new(env_name, env_path, storage_size, logger).await?;
let mut input = domain.as_bytes().to_vec();
input.extend_from_slice(b"root");
let mut password_input = input.clone();
password_input.extend_from_slice(password.as_bytes());
let mut hasher = Sha256::new();
hasher.update(&password_input);
let password = hasher.finalize().to_vec();
let invite_token = api_server.auth.api_invite_get(domain, "root", None)?;
let (state, reg_start_req) = AuthClient::registration_start_req(b"root", &password)?;
let checked_claims = api_server.auth.invite_check(&invite_token)?;
let reg_start_res = api_server.auth.registration_start(
reg_start_req,
None,
None,
Some(checked_claims),
)?;
let (private_key, reg_finish_req) =
AuthClient::registration_finish_req(b"root", &password, &state, ®_start_res)?;
let (reg_finish_res, ..) = api_server.auth.registration_finish(reg_finish_req, None)?;
let (totp, _recovery_codes) = AuthClient::decrypt_totp_mfa(
®_finish_res,
private_key,
env_name.to_string(),
"root".into(),
)?;
Ok((totp.get_url(), totp.secret))
}
.instrument(span.clone())
.await
}
#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
pub async fn new(
env_name: &str,
env_path: impl AsRef<Path>,
storage_size: usize,
logger: Option<OrdinaryLogger>,
) -> anyhow::Result<OrdinaryApiServer> {
let span = tracing::info_span!("setup", env = %env_name, pid = process::id());
async {
let config: OrdinaryApiConfig = serde_json::from_str(&fs_err::read_to_string(
env_path.as_ref().join("ordinaryd.json"),
)?)?;
let data_path = env_path.as_ref().join("data");
fs_err::create_dir_all(&data_path)?;
let ps = page_size::get();
let remainder = storage_size % ps;
let mapsize = (storage_size - remainder) + ps;
tracing::info!(mapsize = %bytesize::ByteSize(mapsize as u64).display().si_short());
let env = Arc::new(unsafe {
let mut env_builder = EnvBuilder::new()?;
env_builder.set_maxreaders(126)?;
env_builder.set_mapsize(mapsize)?;
env_builder.set_maxdbs(13)?;
env_builder.open(
match data_path.to_str() {
Some(v) => v,
None => bail!("data_path not a str"),
},
&saferlmdb::open::Flags::empty(),
0o600,
)?
});
let keys_dir = env_path.as_ref().join("keys");
fs_err::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 = fs_err::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 = fs_err::File::create(auth_key_path)?;
auth_key_file.write_all(&auth_key)?;
auth_key_file.flush()?;
auth_key
};
let auth = Arc::new(Auth::new(
config.domain.clone(),
Some(AuthConfig {
password: PasswordConfig {
protocol: ordinary_config::PasswordProtocol::Opaque,
},
mfa: MfaConfig {
totp: TotpConfig {
template: None,
algorithm: ordinary_config::TotpAlgorithm::Sha1,
},
},
refresh_token: RefreshTokenConfig::default(),
access_token: AccessTokenConfig {
claims: api_account_claims(),
..AccessTokenConfig::default()
},
client_hash: ordinary_config::ClientPasswordHash::Sha256,
cookies_enabled: true,
invite: Some(InviteConfig {
mode: InviteMode::Root,
lifetime: 60 * 60 * 24,
clean_interval: (30, 90),
claims: Some(api_invite_claims()),
}),
}),
auth_key,
env.clone(),
)?);
let apps_dir = env_path.as_ref().join("apps");
fs_err::create_dir_all(&apps_dir)?;
let monitor = Arc::new(logger.map(|logger| {
OrdinaryMonitorService::new(logger).expect("failed to set up monitor service")
}));
Ok(OrdinaryApiServer {
config,
auth,
env,
servers: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
apps_dir,
monitor,
})
}
.instrument(span)
.await
}
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub async fn start<P, F>(
&self,
server_span: Span,
mode: SecurityMode<P>,
listener: TcpListener,
secure_cookies: bool,
log_headers: bool,
log_ips: bool,
log_size: bool,
redirect_listener: Option<TcpListener>,
dedicated_ports: bool,
stored_logs: bool,
redacted_hash: Option<RedactedHashAlg>,
openapi: bool,
swagger: bool,
signal: fn() -> F,
danger_dns_no_verify: bool,
) -> anyhow::Result<()>
where
P: AsRef<Path> + std::clone::Clone,
F: Future<Output = ()> + Send + 'static,
{
start(
self,
server_span,
mode,
listener,
secure_cookies,
log_headers,
log_ips,
log_size,
redirect_listener,
dedicated_ports,
stored_logs,
redacted_hash,
openapi,
swagger,
signal,
danger_dns_no_verify,
)
.await
}
}