use crate::{application, crypto, encoding::base64, extension::TomlTableExt, helper, SharedString};
use std::{
borrow::Cow,
env, fs,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::LazyLock,
};
use toml::value::Table;
mod data;
pub use data::{Data, SharedData};
#[derive(Debug, Clone)]
pub struct State<T = ()> {
env: &'static str,
config: Table,
data: T,
}
impl<T> State<T> {
#[inline]
pub fn new(env: &'static str, data: T) -> Self {
Self {
env,
config: Table::new(),
data,
}
}
pub fn load_config(&mut self) {
let env = self.env;
let config_file = application::PROJECT_DIR.join(format!("./config/config.{env}.toml"));
let config = match fs::read_to_string(&config_file) {
Ok(value) => {
tracing::warn!(env, "`config.{env}.toml` loaded");
value.parse().unwrap_or_default()
}
Err(err) => {
let config_file = config_file.to_string_lossy();
tracing::error!("fail to read the config file `{config_file}`: {err}");
Table::new()
}
};
self.config = config;
}
#[inline]
pub fn set_data(&mut self, data: T) {
self.data = data;
}
#[inline]
pub fn env(&self) -> &'static str {
self.env
}
#[inline]
pub fn config(&self) -> &Table {
&self.config
}
#[inline]
pub fn get_config(&self, key: &str) -> Option<&Table> {
self.config().get_table(key)
}
#[inline]
pub fn get_extension_config(&self, extension: &str) -> Option<&Table> {
self.config().get_table("extensions")?.get_table(extension)
}
#[inline]
pub fn data(&self) -> &T {
&self.data
}
#[inline]
pub fn data_mut(&mut self) -> &mut T {
&mut self.data
}
pub fn listeners(&self) -> Vec<(SharedString, SocketAddr)> {
let config = self.config();
let mut listeners = Vec::new();
if let Some(debug_server) = config.get_table("debug") {
let debug_host = debug_server
.get_str("host")
.and_then(|s| s.parse::<IpAddr>().ok())
.expect("the `debug.host` field should be a str");
let debug_port = debug_server
.get_u16("port")
.expect("the `debug.port` field should be an integer");
listeners.push(("debug".into(), (debug_host, debug_port).into()));
}
if let Some(main_server) = config.get_table("main") {
let main_host = main_server
.get_str("host")
.and_then(|s| s.parse::<IpAddr>().ok())
.expect("the `main.host` field should be a str");
let main_port = main_server
.get_u16("port")
.expect("the `main.port` field should be an integer");
listeners.push(("main".into(), (main_host, main_port).into()));
}
if config.contains_key("standby") {
let standbys = config
.get_array("standby")
.expect("the `standby` field should be an array of tables");
for standby in standbys.iter().filter_map(|v| v.as_table()) {
let server_name = standby
.get_str("name")
.map(|s| s.to_owned().into())
.unwrap_or("standby".into());
let standby_host = standby
.get_str("host")
.and_then(|s| s.parse::<IpAddr>().ok())
.expect("the `standby.host` field should be a str");
let standby_port = standby
.get_u16("port")
.expect("the `standby.port` field should be an integer");
listeners.push((server_name, (standby_host, standby_port).into()));
}
}
if listeners.is_empty() {
listeners.push(("main".into(), (Ipv4Addr::LOCALHOST, 6080).into()));
}
listeners
}
}
impl State {
#[inline]
pub fn shared() -> &'static Self {
LazyLock::force(&SHARED_STATE)
}
pub fn encrypt_password(config: &Table) -> Option<Cow<'_, str>> {
let password = config.get_str("password")?;
application::SECRET_KEY.get().and_then(|key| {
if let Ok(data) = base64::decode(password) &&
crypto::decrypt(&data, key).is_ok()
{
Some(password.into())
} else {
crypto::encrypt(password.as_bytes(), key)
.ok()
.map(|bytes| base64::encode(bytes).into())
}
})
}
pub fn decrypt_password(config: &Table) -> Option<Cow<'_, str>> {
let password = config.get_str("password")?;
if let Ok(data) = base64::decode(password) {
if let Some(key) = application::SECRET_KEY.get() &&
let Ok(plaintext) = crypto::decrypt(&data, key)
{
return Some(String::from_utf8_lossy(&plaintext).into_owned().into());
}
}
if let Some(encrypted_password) = Self::encrypt_password(config).as_deref() {
let num_chars = password.len() / 4;
let masked_password = helper::mask_text(password, num_chars, num_chars);
tracing::warn!(
encrypted_password,
"raw passowrd `{masked_password}` should be encypted"
);
}
Some(password.into())
}
pub fn format_authority(config: &Table, default_port: Option<u16>) -> String {
let mut authority = String::new();
let username = config.get_str("username").unwrap_or_default();
authority += username;
if let Some(password) = Self::decrypt_password(config) {
authority += &format!(":{password}@");
}
let host = config.get_str("host").unwrap_or("localhost");
authority += host;
if let Some(port) = config.get_u16("port").or(default_port) {
authority += &format!(":{port}");
}
authority
}
}
impl<T: Default> Default for State<T> {
#[inline]
fn default() -> Self {
State::new(&DEFAULT_ENV, T::default())
}
}
static DEFAULT_ENV: LazyLock<&'static str> = LazyLock::new(|| {
for arg in env::args().skip(1) {
if let Some(value) = arg.strip_prefix("--env=") {
return value.to_owned().leak();
}
}
if let Ok(value) = env::var("ZINO_APP_ENV") {
return value.to_owned().leak();
}
"dev"
});
static SHARED_STATE: LazyLock<State> = LazyLock::new(|| {
let mut state = State::default();
state.load_config();
state
});