pub mod loader;
pub mod runtime;
pub mod schema;
pub mod secrets;
pub mod validator;
use anyhow::{Context, Result};
use loader::{detect_config_path, load_json5};
use runtime::{IntoRuntime, RuntimeConfig};
pub fn load() -> Result<RuntimeConfig> {
let path = detect_config_path().with_context(
|| "no config file found. Run `rsclaw setup` to create one, or set RSCLAW_CONFIG_PATH.",
)?;
tracing::info!(path = %path.display(), "loading config");
load_from_path(&path)
}
pub fn load_quiet() -> Result<RuntimeConfig> {
let path = detect_config_path().with_context(
|| "no config file found. Run `rsclaw setup` to create one, or set RSCLAW_CONFIG_PATH.",
)?;
load_from_path(&path)
}
fn load_from_path(path: &std::path::Path) -> Result<RuntimeConfig> {
let runtime = load_json5(&path)
.with_context(|| format!("failed to load config: {}", path.display()))?
.into_runtime()?;
validator::validate(&runtime)?;
let runtime = apply_env_overrides(runtime);
Ok(runtime)
}
fn apply_env_overrides(mut cfg: RuntimeConfig) -> RuntimeConfig {
if let Ok(port_str) = std::env::var("RSCLAW_PORT")
&& let Ok(port) = port_str.parse::<u16>()
{
cfg.gateway.port = port;
}
cfg
}
pub fn load_from(path: std::path::PathBuf) -> Result<RuntimeConfig> {
let runtime = load_json5(&path)?.into_runtime()?;
validator::validate(&runtime)?;
Ok(runtime)
}
pub fn resolve_proxy(config: &RuntimeConfig) -> Option<String> {
if let Ok(p) = std::env::var("RSCLAW_PROXY") {
let p = p.trim().to_owned();
if !p.is_empty() { return Some(p); }
}
config.raw.gateway.as_ref()
.and_then(|g| g.proxy.as_ref())
.filter(|p| !p.is_empty())
.cloned()
}
fn resolve_proxy_allow(config: &RuntimeConfig) -> Option<String> {
if let Ok(v) = std::env::var("RSCLAW_PROXY_ALLOW") {
if !v.trim().is_empty() { return Some(v.trim().to_owned()); }
}
config.raw.gateway.as_ref()
.and_then(|g| g.proxy_allow.as_ref())
.filter(|v| !v.is_empty())
.cloned()
}
fn resolve_proxy_deny(config: &RuntimeConfig) -> Option<String> {
if let Ok(v) = std::env::var("RSCLAW_PROXY_DENY") {
if !v.trim().is_empty() { return Some(v.trim().to_owned()); }
}
config.raw.gateway.as_ref()
.and_then(|g| g.proxy_deny.as_ref())
.filter(|v| !v.is_empty())
.cloned()
}
fn host_matches_pattern(host: &str, pattern: &str) -> bool {
let host = host.to_lowercase();
let pattern = pattern.trim().to_lowercase();
if pattern == "*" { return true; }
if pattern.starts_with("*.") {
let suffix = &pattern[1..]; host.ends_with(suffix) || host == pattern[2..]
} else {
host == pattern || host.ends_with(&format!(".{pattern}"))
}
}
fn host_matches_any(host: &str, patterns: &str) -> bool {
patterns.split(',').any(|p| host_matches_pattern(host, p.trim()))
}
pub fn apply_proxy_env(config: &RuntimeConfig) {
let proxy_url = match resolve_proxy(config) {
Some(u) => u,
None => return,
};
let allow = resolve_proxy_allow(config);
let deny = resolve_proxy_deny(config);
let mut deny_list = "localhost,127.0.0.1,::1".to_owned();
if let Some(ref d) = deny {
deny_list = format!("{deny_list},{d}");
}
if allow.is_none() || allow.as_deref() == Some("*") {
unsafe {
std::env::set_var("HTTP_PROXY", &proxy_url);
std::env::set_var("HTTPS_PROXY", &proxy_url);
std::env::set_var("NO_PROXY", &deny_list);
}
tracing::info!(proxy = %proxy_url, deny = %deny_list, "global proxy configured (all domains)");
} else {
unsafe { std::env::set_var("NO_PROXY", &deny_list); }
PROXY_ALLOW.get_or_init(|| allow.clone().unwrap_or_default());
PROXY_DENY.get_or_init(|| deny_list.clone());
PROXY_URL.get_or_init(|| proxy_url.clone());
tracing::info!(proxy = %proxy_url, allow = ?allow, deny = %deny_list, "global proxy configured (allow-list mode, selective)");
}
}
static PROXY_ALLOW: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static PROXY_DENY: std::sync::OnceLock<String> = std::sync::OnceLock::new();
static PROXY_URL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
pub fn build_proxy_client() -> reqwest::ClientBuilder {
let mut builder = reqwest::Client::builder();
let allow = PROXY_ALLOW.get().map(|s| s.as_str()).unwrap_or("");
let proxy_url = PROXY_URL.get().map(|s| s.as_str()).unwrap_or("");
let deny = PROXY_DENY.get().map(|s| s.as_str()).unwrap_or("");
if !proxy_url.is_empty() && !allow.is_empty() && allow != "*" {
let allow_owned = allow.to_owned();
let deny_owned = deny.to_owned();
let url_owned = proxy_url.to_owned();
let proxy = reqwest::Proxy::custom(move |url| {
let host = url.host_str().unwrap_or("");
if !deny_owned.is_empty() && host_matches_any(host, &deny_owned) {
return None;
}
if host_matches_any(host, &allow_owned) {
Some(url_owned.clone())
} else {
None
}
});
builder = builder.proxy(proxy);
}
builder
}
pub fn system_tz() -> chrono_tz::Tz {
if let Ok(tz_name) = std::env::var("TZ") {
if let Ok(tz) = tz_name.parse() {
return tz;
}
}
let local_offset = chrono::Local::now().offset().local_minus_utc();
match local_offset {
25200 => chrono_tz::Asia::Bangkok, 28800 => chrono_tz::Asia::Shanghai, 32400 => chrono_tz::Asia::Tokyo, 36000 => chrono_tz::Australia::Sydney, -18000 => chrono_tz::US::Eastern, -21600 => chrono_tz::US::Central, -25200 => chrono_tz::US::Mountain, -28800 => chrono_tz::US::Pacific, 0 => chrono_tz::UTC,
_ => {
tracing::warn!(offset_secs = local_offset, "unknown system timezone offset, using UTC. Set TZ env var for accuracy.");
chrono_tz::UTC
}
}
}