Improve config system

Add pkl to generate configs
This commit is contained in:
Tobias Reisinger 2024-02-18 19:50:22 +01:00
parent 8785186dfa
commit b2ff632e64
Signed by: serguzim
GPG key ID: 13AD60C237A28DFE
47 changed files with 325 additions and 262 deletions

3
.cargo/config.toml Normal file
View file

@ -0,0 +1,3 @@
[alias]
format = "+nightly fmt"
lint = "clippy --all-targets --all-features -- -D warnings"

2
.env
View file

@ -1 +1 @@
DATABASE_URL="sqlite://emgauwa-dev.sqlite" DATABASE_URL="sqlite://_local/emgauwa-dev.sqlite"

2
.gitignore vendored
View file

@ -4,8 +4,10 @@
/tests/testing_bak/ /tests/testing_bak/
/tests/testing_latest/ /tests/testing_latest/
/_local/
emgauwa-*.sqlite emgauwa-*.sqlite
emgauwa-*.sqlite-* emgauwa-*.sqlite-*
emgauwa-*.json
# Added by cargo # Added by cargo

View file

@ -1,24 +1,15 @@
sqlx:
build: cargo sqlx database drop
cargo build
sqlx-prepare:
rm -f ./emgauwa-dev.sqlite
cargo sqlx database create cargo sqlx database create
cargo sqlx migrate run cargo sqlx migrate run
sqlx: sqlx-prepare
cargo sqlx prepare --workspace cargo sqlx prepare --workspace
build-rpi: build-rpi:
cross build --target arm-unknown-linux-gnueabihf cross build --target arm-unknown-linux-gnueabihf
clean-db: _local/emgauwa-%.json: config/emgauwa-%.pkl config/lib/%.pkl config/lib/common.pkl
rm ./emgauwa-*.sqlite || true pkl eval -f json -o $@ $<
$(MAKE) sqlx-prepare
format: configs:
cargo +nightly fmt $(MAKE) _local/emgauwa-core.json
$(MAKE) _local/emgauwa-controller.json
lint:
cargo clippy --all-targets --all-features -- -D warnings

View file

@ -0,0 +1,71 @@
amends "./lib/controller.pkl"
server {
host = "127.0.0.1"
port = 4419
}
database = "sqlite://_local/emgauwa-controller.sqlite"
permissions {
user = "emgauwa"
group = "emgauwa"
}
logging {
level = "DEBUG"
file = "stdout"
}
relays {
new {
driver = "gpio"
pin = 5
inverted = true
}
new {
driver = "gpio"
pin = 4
inverted = true
}
new {
driver = "gpio"
pin = 3
inverted = true
}
new {
driver = "gpio"
pin = 2
inverted = true
}
new {
driver = "gpio"
pin = 1
inverted = true
}
new {
driver = "gpio"
pin = 0
inverted = true
}
new {
driver = "gpio"
pin = 16
inverted = true
}
new {
driver = "gpio"
pin = 15
inverted = true
}
new {
driver = "piface"
pin = 1
inverted = false
}
new {
driver = "piface"
pin = 0
inverted = false
}
}

18
config/emgauwa-core.pkl Normal file
View file

@ -0,0 +1,18 @@
amends "./lib/core.pkl"
server {
host = "127.0.0.1"
port = 4419
}
database = "sqlite://_local/emgauwa-core.sqlite"
permissions {
user = "emgauwa"
group = "emgauwa"
}
logging {
level = "DEBUG"
file = "stdout"
}

15
config/lib/common.pkl Normal file
View file

@ -0,0 +1,15 @@
class ServerConfig {
host: String
port: UInt16
}
/// Set to a user and a group to drop privileges to after binding to the port
class PermissionsConfig {
user: String
group: String
}
class LoggingConfig {
level: String
file: String
}

16
config/lib/controller.pkl Normal file
View file

@ -0,0 +1,16 @@
import "./common.pkl"
server: common.ServerConfig
database: String
permissions: common.PermissionsConfig
logging: common.LoggingConfig
class RelayConfig {
driver: "gpio" | "piface"
pin: Number
inverted: Boolean
}
relays: Listing<RelayConfig>

12
config/lib/core.pkl Normal file
View file

@ -0,0 +1,12 @@
import "./common.pkl"
server: common.ServerConfig
database: String
permissions: common.PermissionsConfig
logging: common.LoggingConfig
/// Leave empty to allow all origins (will always respond with Origin and not "*")
origins: Listing<String>

View file

@ -1,60 +0,0 @@
database = "sqlite://emgauwa-controller.sqlite"
name = "Emgauwa Controller"
[core]
port = 4419
host = "127.0.0.1"
[logging]
level = "DEBUG"
file = "stdout"
[[relays]]
driver = "gpio"
pin = 5
inverted = 1
[[relays]]
driver = "gpio"
pin = 4
inverted = 1
[[relays]]
driver = "gpio"
pin = 3
inverted = 1
[[relays]]
driver = "gpio"
pin = 2
inverted = 1
[[relays]]
driver = "gpio"
pin = 1
inverted = 1
[[relays]]
driver = "gpio"
pin = 0
inverted = 1
[[relays]]
driver = "gpio"
pin = 16
inverted = 1
[[relays]]
driver = "gpio"
pin = 15
inverted = 1
[[relays]]
driver = "piface"
pin = 1
inverted = 0
[[relays]]
driver = "piface"
pin = 0
inverted = 0

View file

@ -4,7 +4,7 @@ use emgauwa_lib::db::{DbController, DbJunctionRelaySchedule, DbRelay, DbSchedule
use emgauwa_lib::errors::EmgauwaError; use emgauwa_lib::errors::EmgauwaError;
use emgauwa_lib::models::{Controller, FromDbModel}; use emgauwa_lib::models::{Controller, FromDbModel};
use emgauwa_lib::types::ControllerUid; use emgauwa_lib::types::ControllerUid;
use emgauwa_lib::utils::init_logging; use emgauwa_lib::utils::{drop_privileges, init_logging};
use sqlx::pool::PoolConnection; use sqlx::pool::PoolConnection;
use sqlx::Sqlite; use sqlx::Sqlite;
@ -59,6 +59,9 @@ async fn create_this_relay(
#[actix::main] #[actix::main]
async fn main() -> Result<(), std::io::Error> { async fn main() -> Result<(), std::io::Error> {
let settings = settings::init()?; let settings = settings::init()?;
drop_privileges(&settings.permissions)?;
init_logging(&settings.logging.level)?; init_logging(&settings.logging.level)?;
let pool = db::init(&settings.database) let pool = db::init(&settings.database)
@ -105,7 +108,7 @@ async fn main() -> Result<(), std::io::Error> {
let url = format!( let url = format!(
"ws://{}:{}/api/v1/ws/controllers", "ws://{}:{}/api/v1/ws/controllers",
settings.core.host, settings.core.port settings.server.host, settings.server.port
); );

View file

@ -1,25 +1,9 @@
use emgauwa_lib::errors::EmgauwaError; use emgauwa_lib::errors::EmgauwaError;
use emgauwa_lib::{constants, utils}; use emgauwa_lib::settings;
use serde_derive::Deserialize; use serde_derive::Deserialize;
use crate::driver::Driver; use crate::driver::Driver;
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Core {
pub host: String,
pub port: u16,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Logging {
pub level: String,
pub file: String,
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(default)] #[serde(default)]
#[allow(unused)] #[allow(unused)]
@ -35,9 +19,11 @@ pub struct Relay {
#[serde(default)] #[serde(default)]
#[allow(unused)] #[allow(unused)]
pub struct Settings { pub struct Settings {
pub core: Core, pub server: settings::Server,
pub database: String, pub database: String,
pub logging: Logging, pub permissions: settings::Permissions,
pub logging: settings::Logging,
pub name: String, pub name: String,
pub relays: Vec<Relay>, pub relays: Vec<Relay>,
} }
@ -45,9 +31,11 @@ pub struct Settings {
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Settings { Settings {
core: Core::default(), server: settings::Server::default(),
database: String::from("sqlite://emgauwa-controller.sqlite"), database: String::from("sqlite://_local/emgauwa-controller.sqlite"),
logging: Logging::default(), permissions: settings::Permissions::default(),
logging: settings::Logging::default(),
name: String::from("Emgauwa Controller"), name: String::from("Emgauwa Controller"),
relays: Vec::new(), relays: Vec::new(),
} }
@ -66,26 +54,8 @@ impl Default for Relay {
} }
} }
impl Default for Core {
fn default() -> Self {
Core {
host: String::from("127.0.0.1"),
port: constants::DEFAULT_PORT,
}
}
}
impl Default for Logging {
fn default() -> Self {
Logging {
level: String::from("info"),
file: String::from("stdout"),
}
}
}
pub fn init() -> Result<Settings, EmgauwaError> { pub fn init() -> Result<Settings, EmgauwaError> {
let mut settings: Settings = utils::load_settings("controller", "CONTROLLER")?; let mut settings: Settings = settings::load("controller", "CONTROLLER")?;
for (num, relay) in settings.relays.iter_mut().enumerate() { for (num, relay) in settings.relays.iter_mut().enumerate() {
if relay.number.is_none() { if relay.number.is_none() {

View file

@ -1,15 +0,0 @@
port = 4419
host = "127.0.0.1"
# Set to a user and a group to drop privileges to after binding to the port
#user = "emgauwa"
#group = "emgauwa"
# Leave empty to allow all origins (will always respond with Origin and not "*")
#origins = ["http://localhost", "https://emgauwa.app"]
database = "sqlite://emgauwa-core.sqlite"
[logging]
level = "DEBUG"
file = "stdout"

View file

@ -24,4 +24,3 @@ serde_derive = "1.0"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chrono"] }
futures = "0.3.29" futures = "0.3.29"
libc = "0.2"

View file

@ -1,3 +1,3 @@
fn main() { fn main() {
println!("cargo:rustc-env=DATABASE_URL=sqlite://emgauwa-core.sqlite") println!("cargo:rustc-env=DATABASE_URL=sqlite://_local/emgauwa-core.sqlite")
} }

View file

@ -6,10 +6,9 @@ use actix_web::middleware::TrailingSlash;
use actix_web::{middleware, web, App, HttpServer}; use actix_web::{middleware, web, App, HttpServer};
use emgauwa_lib::db::DbController; use emgauwa_lib::db::DbController;
use emgauwa_lib::errors::EmgauwaError; use emgauwa_lib::errors::EmgauwaError;
use emgauwa_lib::utils::init_logging; use emgauwa_lib::utils::{drop_privileges, init_logging};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::utils::drop_privileges;
mod app_state; mod app_state;
mod handlers; mod handlers;
@ -19,12 +18,12 @@ mod utils;
#[actix_web::main] #[actix_web::main]
async fn main() -> Result<(), std::io::Error> { async fn main() -> Result<(), std::io::Error> {
let settings = settings::init()?; let settings = settings::init()?;
let listener = TcpListener::bind(format!("{}:{}", settings.server.host, settings.server.port))?;
drop_privileges(&settings.permissions)?;
init_logging(&settings.logging.level)?; init_logging(&settings.logging.level)?;
let listener = TcpListener::bind(format!("{}:{}", settings.host, settings.port))?;
drop_privileges(&settings)?;
let pool = emgauwa_lib::db::init(&settings.database).await?; let pool = emgauwa_lib::db::init(&settings.database).await?;
let mut conn = pool.acquire().await.map_err(EmgauwaError::from)?; let mut conn = pool.acquire().await.map_err(EmgauwaError::from)?;
@ -35,7 +34,11 @@ async fn main() -> Result<(), std::io::Error> {
let app_state = AppState::new(pool.clone()).start(); let app_state = AppState::new(pool.clone()).start();
log::info!("Starting server on {}:{}", settings.host, settings.port); log::info!(
"Starting server on {}:{}",
settings.server.host,
settings.server.port
);
HttpServer::new(move || { HttpServer::new(move || {
let cors = Cors::default().allow_any_method().allow_any_header(); let cors = Cors::default().allow_any_method().allow_any_header();

View file

@ -1,57 +1,32 @@
use emgauwa_lib::errors::EmgauwaError; use emgauwa_lib::errors::EmgauwaError;
use emgauwa_lib::{constants, utils}; use emgauwa_lib::settings;
use serde_derive::Deserialize; use serde_derive::Deserialize;
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Logging {
pub level: String,
pub file: String,
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(default)] #[serde(default)]
#[allow(unused)] #[allow(unused)]
pub struct Settings { pub struct Settings {
pub server: settings::Server,
pub database: String, pub database: String,
pub permissions: settings::Permissions,
pub logging: settings::Logging,
pub host: String,
pub port: u16,
pub origins: Vec<String>, pub origins: Vec<String>,
pub user: String,
pub group: String,
pub logging: Logging,
} }
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Settings { Settings {
database: String::from("sqlite://emgauwa-core.sqlite"), server: settings::Server::default(),
database: String::from("sqlite://_local/emgauwa-core.sqlite"),
permissions: settings::Permissions::default(),
logging: settings::Logging::default(),
host: String::from("127.0.0.1"),
port: constants::DEFAULT_PORT,
origins: Vec::new(), origins: Vec::new(),
user: String::from(""),
group: String::from(""),
logging: Logging::default(),
}
}
}
impl Default for Logging {
fn default() -> Self {
Logging {
level: String::from("info"),
file: String::from("stdout"),
} }
} }
} }
pub fn init() -> Result<Settings, EmgauwaError> { pub fn init() -> Result<Settings, EmgauwaError> {
utils::load_settings("core", "CORE") settings::load("core", "CORE")
} }

View file

@ -1,8 +1,3 @@
use std::ffi::CString;
use std::io::{Error, ErrorKind};
use crate::settings::Settings;
pub fn flatten_result<T, E>(res: Result<Result<T, E>, E>) -> Result<T, E> { pub fn flatten_result<T, E>(res: Result<Result<T, E>, E>) -> Result<T, E> {
match res { match res {
Ok(Ok(t)) => Ok(t), Ok(Ok(t)) => Ok(t),
@ -10,64 +5,3 @@ pub fn flatten_result<T, E>(res: Result<Result<T, E>, E>) -> Result<T, E> {
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
// https://blog.lxsang.me/post/id/28.0
pub fn drop_privileges(settings: &Settings) -> Result<(), Error> {
log::info!(
"Dropping privileges to {}:{}",
settings.user,
settings.group
);
// the group id need to be set first, because, when the user privileges drop,
// we are unable to drop the group privileges
if !settings.group.is_empty() {
drop_privileges_group(&settings.group)?;
}
if !settings.user.is_empty() {
drop_privileges_user(&settings.user)?;
}
Ok(())
}
fn drop_privileges_group(group: &str) -> Result<(), Error> {
// get the gid from username
if let Ok(cstr) = CString::new(group.as_bytes()) {
let p = unsafe { libc::getgrnam(cstr.as_ptr()) };
if p.is_null() {
log::error!("Unable to getgrnam of group: {}", group);
return Err(Error::last_os_error());
}
if unsafe { libc::setgid((*p).gr_gid) } != 0 {
log::error!("Unable to setgid of group: {}", group);
return Err(Error::last_os_error());
}
} else {
return Err(Error::new(
ErrorKind::Other,
"Cannot create CString from String (group)!",
));
}
Ok(())
}
fn drop_privileges_user(user: &str) -> Result<(), Error> {
// get the uid from username
if let Ok(cstr) = CString::new(user.as_bytes()) {
let p = unsafe { libc::getpwnam(cstr.as_ptr()) };
if p.is_null() {
log::error!("Unable to getpwnam of user: {}", user);
return Err(Error::last_os_error());
}
if unsafe { libc::setuid((*p).pw_uid) } != 0 {
log::error!("Unable to setuid of user: {}", user);
return Err(Error::last_os_error());
}
} else {
return Err(Error::new(
ErrorKind::Other,
"Cannot create CString from String (user)!",
));
}
Ok(())
}

View file

@ -25,3 +25,4 @@ sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio", "macros", "chro
libsqlite3-sys = { version = "*", features = ["bundled"] } libsqlite3-sys = { version = "*", features = ["bundled"] }
uuid = "1.6" uuid = "1.6"
futures = "0.3" futures = "0.3"
libc = "0.2"

View file

@ -1,4 +1,4 @@
fn main() { fn main() {
println!("cargo:rerun-if-changed=migrations"); println!("cargo:rerun-if-changed=migrations");
println!("cargo:rustc-env=DATABASE_URL=sqlite://emgauwa-dev.sqlite"); println!("cargo:rustc-env=DATABASE_URL=sqlite://_local/emgauwa-dev.sqlite");
} }

View file

@ -2,5 +2,6 @@ pub mod constants;
pub mod db; pub mod db;
pub mod errors; pub mod errors;
pub mod models; pub mod models;
pub mod settings;
pub mod types; pub mod types;
pub mod utils; pub mod utils;

View file

@ -0,0 +1,77 @@
use serde_derive::Deserialize;
use crate::constants;
use crate::errors::EmgauwaError;
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Server {
pub host: String,
pub port: u16,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Logging {
pub level: String,
pub file: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(default)]
#[allow(unused)]
pub struct Permissions {
pub user: String,
pub group: String,
}
impl Default for Server {
fn default() -> Self {
Server {
host: String::from("127.0.0.1"),
port: constants::DEFAULT_PORT,
}
}
}
impl Default for Logging {
fn default() -> Self {
Logging {
level: String::from("info"),
file: String::from("stdout"),
}
}
}
impl Default for Permissions {
fn default() -> Self {
Permissions {
user: String::from(""),
group: String::from(""),
}
}
}
pub fn load<T>(config_name: &str, env_prefix: &str) -> Result<T, EmgauwaError>
where
for<'de> T: serde::Deserialize<'de>,
{
// TODO: Add switch to only include local config if in development mode
let dev_file =
config::File::with_name(&format!("./_local/emgauwa-{}", config_name)).required(false);
let local_file = config::File::with_name(&format!("./emgauwa-{}", config_name)).required(false);
config::Config::builder()
.add_source(dev_file)
.add_source(local_file)
.add_source(
config::Environment::with_prefix(&format!("EMGAUWA_{}", env_prefix))
.prefix_separator("__")
.separator("__"),
)
.build()?
.try_deserialize::<T>()
.map_err(EmgauwaError::from)
}

View file

@ -1,3 +1,5 @@
use std::ffi::CString;
use std::io::{Error, ErrorKind};
use std::str::FromStr; use std::str::FromStr;
use chrono::Datelike; use chrono::Datelike;
@ -5,25 +7,9 @@ use log::LevelFilter;
use simple_logger::SimpleLogger; use simple_logger::SimpleLogger;
use crate::errors::EmgauwaError; use crate::errors::EmgauwaError;
use crate::settings::Permissions;
use crate::types::Weekday; use crate::types::Weekday;
pub fn load_settings<T>(config_name: &str, env_prefix: &str) -> Result<T, EmgauwaError>
where
for<'de> T: serde::Deserialize<'de>,
{
let default_file = config::File::with_name(&format!("emgauwa-{}", config_name)).required(false);
config::Config::builder()
.add_source(default_file)
.add_source(
config::Environment::with_prefix(&format!("EMGAUWA_{}", env_prefix))
.prefix_separator("__")
.separator("__"),
)
.build()?
.try_deserialize::<T>()
.map_err(EmgauwaError::from)
}
pub fn init_logging(level: &str) -> Result<(), EmgauwaError> { pub fn init_logging(level: &str) -> Result<(), EmgauwaError> {
let log_level: LevelFilter = LevelFilter::from_str(level) let log_level: LevelFilter = LevelFilter::from_str(level)
@ -38,6 +24,67 @@ pub fn init_logging(level: &str) -> Result<(), EmgauwaError> {
Ok(()) Ok(())
} }
// https://blog.lxsang.me/post/id/28.0
pub fn drop_privileges(permissions: &Permissions) -> Result<(), Error> {
log::info!(
"Dropping privileges to {}:{}",
permissions.user,
permissions.group
);
// the group id need to be set first, because, when the user privileges drop,
// we are unable to drop the group privileges
if !permissions.group.is_empty() {
drop_privileges_group(&permissions.group)?;
}
if !permissions.user.is_empty() {
drop_privileges_user(&permissions.user)?;
}
Ok(())
}
fn drop_privileges_group(group: &str) -> Result<(), Error> {
// get the gid from username
if let Ok(cstr) = CString::new(group.as_bytes()) {
let p = unsafe { libc::getgrnam(cstr.as_ptr()) };
if p.is_null() {
log::error!("Unable to getgrnam of group: {}", group);
return Err(Error::last_os_error());
}
if unsafe { libc::setgid((*p).gr_gid) } != 0 {
log::error!("Unable to setgid of group: {}", group);
return Err(Error::last_os_error());
}
} else {
return Err(Error::new(
ErrorKind::Other,
"Cannot create CString from String (group)!",
));
}
Ok(())
}
fn drop_privileges_user(user: &str) -> Result<(), Error> {
// get the uid from username
if let Ok(cstr) = CString::new(user.as_bytes()) {
let p = unsafe { libc::getpwnam(cstr.as_ptr()) };
if p.is_null() {
log::error!("Unable to getpwnam of user: {}", user);
return Err(Error::last_os_error());
}
if unsafe { libc::setuid((*p).pw_uid) } != 0 {
log::error!("Unable to setuid of user: {}", user);
return Err(Error::last_os_error());
}
} else {
return Err(Error::new(
ErrorKind::Other,
"Cannot create CString from String (user)!",
));
}
Ok(())
}
pub fn get_weekday() -> Weekday { pub fn get_weekday() -> Weekday {
(chrono::offset::Local::now() (chrono::offset::Local::now()
.date_naive() .date_naive()