Skip to main content

shared/
env.rs

1use anyhow::Context;
2use axum::{extract::ConnectInfo, http::HeaderMap};
3use colored::Colorize;
4use dotenvy::dotenv;
5use std::sync::{Arc, atomic::AtomicBool};
6use tracing_subscriber::{
7    Layer,
8    filter::{LevelFilter, Targets},
9    fmt::writer::MakeWriterExt,
10    layer::{Layered, SubscriberExt},
11    util::SubscriberInitExt,
12};
13
14#[derive(Clone)]
15pub enum RedisMode {
16    Redis {
17        redis_url: Option<String>,
18    },
19    Sentinel {
20        cluster_name: String,
21        redis_sentinels: Vec<String>,
22    },
23}
24
25impl std::fmt::Display for RedisMode {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            RedisMode::Redis { .. } => write!(f, "Redis"),
29            RedisMode::Sentinel { .. } => write!(f, "Sentinel"),
30        }
31    }
32}
33
34pub struct EnvGuard(
35    pub Option<tracing_appender::non_blocking::WorkerGuard>,
36    pub tracing_appender::non_blocking::WorkerGuard,
37);
38
39type ReloadHandle =
40    tracing_subscriber::reload::Handle<Targets, Layered<LevelFilter, tracing_subscriber::Registry>>;
41
42fn log_filter(debug: bool) -> Targets {
43    let crate_level = if debug {
44        LevelFilter::DEBUG
45    } else {
46        LevelFilter::INFO
47    };
48
49    Targets::new()
50        .with_default(LevelFilter::INFO)
51        .with_target("backend", crate_level)
52        .with_target("database_migrator", crate_level)
53        .with_target("shared", crate_level)
54        .with_target("panel_rs", crate_level)
55        .with_target("panel_rs_aio", crate_level)
56}
57
58pub struct Env {
59    log_reload_handle: ReloadHandle,
60
61    pub redis_mode: RedisMode,
62
63    pub sentry_url: Option<String>,
64    pub database_migrate: bool,
65    pub database_url: String,
66    pub database_url_primary: Option<String>,
67
68    pub bind: String,
69    pub port: u16,
70
71    pub aio_base_wings_configuration: Option<String>,
72
73    pub app_primary: bool,
74    pub app_debug_default: bool,
75    app_debug: AtomicBool,
76    pub app_enable_wings_proxy: bool,
77    pub app_disable_frontend: bool,
78    pub app_use_decryption_cache: bool,
79    pub app_use_internal_cache: bool,
80    pub app_trusted_proxies: Vec<cidr::IpCidr>,
81    pub app_log_directory: Option<String>,
82    pub app_encryption_key: String,
83    pub server_name: Option<String>,
84}
85
86impl Env {
87    pub fn parse() -> Result<(Arc<Self>, EnvGuard), anyhow::Error> {
88        dotenv().ok();
89
90        let redis_mode = match std::env::var("REDIS_MODE")
91            .unwrap_or("redis".to_string())
92            .trim_matches('"')
93        {
94            "redis" => RedisMode::Redis {
95                redis_url: std::env::var("REDIS_URL")
96                    .ok()
97                    .map(|s| s.trim_matches('"').to_string()),
98            },
99            "sentinel" => RedisMode::Sentinel {
100                cluster_name: std::env::var("REDIS_SENTINEL_CLUSTER")
101                    .context("REDIS_SENTINEL_CLUSTER is required")?
102                    .trim_matches('"')
103                    .to_string(),
104                redis_sentinels: std::env::var("REDIS_SENTINELS")
105                    .context("REDIS_SENTINELS is required")?
106                    .trim_matches('"')
107                    .split(',')
108                    .map(|s| s.to_string())
109                    .collect(),
110            },
111            _ => {
112                return Err(anyhow::anyhow!(
113                    "Invalid REDIS_MODE. Expected 'redis' or 'sentinel'."
114                ));
115            }
116        };
117
118        let app_debug_default = std::env::var("APP_DEBUG")
119            .unwrap_or("false".to_string())
120            .trim_matches('"')
121            .parse()
122            .context("Invalid APP_DEBUG value")?;
123
124        let app_encryption_key = std::env::var("APP_ENCRYPTION_KEY")
125            .expect("APP_ENCRYPTION_KEY is required")
126            .trim_matches('"')
127            .to_string();
128
129        if app_encryption_key.to_lowercase() == "changeme" {
130            println!(
131                "{}", "You are using the default APP_ENCRYPTION_KEY. This is unsupported, please modify your .env or your docker compose file.".red()
132            );
133            std::process::exit(1);
134        }
135
136        let app_log_directory = std::env::var("APP_LOG_DIRECTORY")
137            .ok()
138            .map(|s| s.trim_matches('"').to_string());
139
140        let (stdout_writer, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
141
142        let (appender, file_guard) = if let Some(app_log_directory) = &app_log_directory {
143            if !std::path::Path::new(app_log_directory).exists() {
144                std::fs::create_dir_all(app_log_directory)
145                    .context("failed to create log directory")?;
146            }
147
148            let latest_log_path = std::path::Path::new(&app_log_directory).join("panel.log");
149            let latest_file = std::fs::OpenOptions::new()
150                .create(true)
151                .append(true)
152                .open(&latest_log_path)
153                .context("failed to open latest log file")?;
154
155            let rolling_appender = tracing_appender::rolling::Builder::new()
156                .filename_prefix("panel")
157                .filename_suffix("log")
158                .max_log_files(30)
159                .rotation(tracing_appender::rolling::Rotation::DAILY)
160                .build(app_log_directory)
161                .context("failed to create rolling log file appender")?;
162
163            let (appender, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
164                .buffered_lines_limit(50)
165                .finish(latest_file.and(rolling_appender));
166
167            (Some(appender), Some(guard))
168        } else {
169            (None, None)
170        };
171
172        let initial_filter = log_filter(app_debug_default);
173        let (reload_layer, log_reload_handle) =
174            tracing_subscriber::reload::Layer::new(initial_filter);
175
176        let fmt_layer = tracing_subscriber::fmt::layer()
177            .with_timer(tracing_subscriber::fmt::time::ChronoLocal::new(
178                "%Y-%m-%d %H:%M:%S %z".to_string(),
179            ))
180            .with_target(false)
181            .with_level(true)
182            .with_file(true)
183            .with_line_number(true);
184
185        let fmt_layer = if let Some(file_appender) = appender {
186            fmt_layer
187                .with_writer(stdout_writer.and(file_appender))
188                .boxed()
189        } else {
190            fmt_layer.with_writer(stdout_writer).boxed()
191        };
192
193        tracing_subscriber::registry()
194            .with(LevelFilter::DEBUG)
195            .with(reload_layer)
196            .with(fmt_layer)
197            .try_init()
198            .context("failed to install tracing subscriber")?;
199
200        let env = Self {
201            log_reload_handle,
202
203            redis_mode,
204
205            sentry_url: std::env::var("SENTRY_URL")
206                .ok()
207                .map(|s| s.trim_matches('"').to_string()),
208            database_migrate: std::env::var("DATABASE_MIGRATE")
209                .unwrap_or("false".to_string())
210                .trim_matches('"')
211                .parse()
212                .unwrap(),
213            database_url: std::env::var("DATABASE_URL")
214                .context("DATABASE_URL is required")?
215                .trim_matches('"')
216                .to_string(),
217            database_url_primary: std::env::var("DATABASE_URL_PRIMARY")
218                .ok()
219                .map(|s| s.trim_matches('"').to_string()),
220
221            bind: std::env::var("BIND")
222                .unwrap_or("0.0.0.0".to_string())
223                .trim_matches('"')
224                .to_string(),
225            port: std::env::var("PORT")
226                .unwrap_or("8000".to_string())
227                .parse()
228                .context("Invalid PORT value")?,
229
230            aio_base_wings_configuration: std::env::var("AIO_BASE_WINGS_CONFIGURATION")
231                .ok()
232                .map(|s| s.trim_matches('"').to_string()),
233
234            app_primary: std::env::var("APP_PRIMARY")
235                .unwrap_or("true".to_string())
236                .trim_matches('"')
237                .parse()
238                .context("Invalid APP_PRIMARY value")?,
239            app_debug_default,
240            app_debug: AtomicBool::new(app_debug_default),
241            app_enable_wings_proxy: std::env::var("APP_ENABLE_WINGS_PROXY")
242                .unwrap_or("false".to_string())
243                .trim_matches('"')
244                .parse()
245                .context("Invalid APP_ENABLE_WINGS_PROXY value")?,
246            app_disable_frontend: std::env::var("APP_DISABLE_FRONTEND")
247                .unwrap_or("false".to_string())
248                .trim_matches('"')
249                .parse()
250                .context("Invalid APP_DISABLE_FRONTEND value")?,
251            app_use_decryption_cache: std::env::var("APP_USE_DECRYPTION_CACHE")
252                .unwrap_or("false".to_string())
253                .trim_matches('"')
254                .parse()
255                .context("Invalid APP_USE_DECRYPTION_CACHE value")?,
256            app_use_internal_cache: std::env::var("APP_USE_INTERNAL_CACHE")
257                .unwrap_or("true".to_string())
258                .trim_matches('"')
259                .parse()
260                .context("Invalid APP_USE_INTERNAL_CACHE value")?,
261            app_trusted_proxies: std::env::var("APP_TRUSTED_PROXIES")
262                .unwrap_or("".to_string())
263                .trim_matches('"')
264                .split(',')
265                .filter_map(|s| if s.is_empty() { None } else { s.parse().ok() })
266                .collect(),
267            app_log_directory,
268            app_encryption_key,
269            server_name: std::env::var("SERVER_NAME")
270                .ok()
271                .map(|s| s.trim_matches('"').to_string()),
272        };
273
274        Ok((Arc::new(env), EnvGuard(file_guard, stdout_guard)))
275    }
276
277    #[inline]
278    pub fn find_ip(
279        &self,
280        headers: &HeaderMap,
281        connect_info: ConnectInfo<std::net::SocketAddr>,
282    ) -> std::net::IpAddr {
283        for cidr in &self.app_trusted_proxies {
284            if cidr.contains(&connect_info.ip()) {
285                if let Some(forwarded) = headers.get("X-Forwarded-For")
286                    && let Ok(forwarded) = forwarded.to_str()
287                    && let Some(ip) = forwarded.split(',').next()
288                {
289                    return ip.parse().unwrap_or_else(|_| connect_info.ip());
290                }
291
292                if let Some(forwarded) = headers.get("X-Real-IP")
293                    && let Ok(forwarded) = forwarded.to_str()
294                {
295                    return forwarded.parse().unwrap_or_else(|_| connect_info.ip());
296                }
297            }
298        }
299
300        connect_info.ip()
301    }
302
303    #[inline]
304    pub fn is_debug(&self) -> bool {
305        self.app_debug.load(std::sync::atomic::Ordering::Relaxed)
306    }
307
308    pub fn set_debug(&self, debug: bool) -> Result<(), anyhow::Error> {
309        self.app_debug
310            .store(debug, std::sync::atomic::Ordering::Relaxed);
311
312        self.log_reload_handle
313            .modify(|filter| *filter = log_filter(debug))
314            .context("failed to reload tracing level filter")?;
315
316        Ok(())
317    }
318}