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}