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
58fn default_blocked_cidrs() -> Vec<cidr::IpCidr> {
59 const DEFAULTS: [&str; 10] = [
60 "0.0.0.0/8",
61 "127.0.0.0/8",
62 "10.0.0.0/8",
63 "100.64.0.0/10",
64 "172.16.0.0/12",
65 "192.168.0.0/16",
66 "169.254.0.0/16",
67 "::1/128",
68 "fe80::/10",
69 "fc00::/7",
70 ];
71
72 DEFAULTS
73 .iter()
74 .map(|cidr| cidr.parse().expect("invalid default blocked cidr"))
75 .collect()
76}
77
78pub struct Env {
79 log_reload_handle: ReloadHandle,
80
81 pub redis_mode: RedisMode,
82
83 pub sentry_url: Option<String>,
84 pub sentry_tracing_sample_rate: f32,
85
86 pub database_migrate: bool,
87 pub database_url: String,
88 pub database_url_primary: Option<String>,
89
90 pub bind: String,
91 pub port: u16,
92
93 pub aio_base_wings_configuration: Option<String>,
94
95 pub app_primary: bool,
96 pub app_debug_default: bool,
97 app_debug: AtomicBool,
98 pub app_enable_wings_proxy: bool,
99 pub app_disable_frontend: bool,
100 pub app_use_decryption_cache: bool,
101 pub app_use_internal_cache: bool,
102 pub app_trusted_proxies: Vec<cidr::IpCidr>,
103 pub app_blocked_cidrs: Vec<cidr::IpCidr>,
104 pub app_log_directory: Option<String>,
105 pub app_encryption_key: String,
106 pub server_name: Option<String>,
107}
108
109impl Env {
110 pub fn parse() -> Result<(Arc<Self>, EnvGuard), anyhow::Error> {
111 dotenv().ok();
112
113 let redis_mode = match std::env::var("REDIS_MODE")
114 .unwrap_or("redis".to_string())
115 .trim_matches('"')
116 {
117 "redis" => RedisMode::Redis {
118 redis_url: std::env::var("REDIS_URL")
119 .ok()
120 .map(|s| s.trim_matches('"').to_string()),
121 },
122 "sentinel" => RedisMode::Sentinel {
123 cluster_name: std::env::var("REDIS_SENTINEL_CLUSTER")
124 .context("REDIS_SENTINEL_CLUSTER is required")?
125 .trim_matches('"')
126 .to_string(),
127 redis_sentinels: std::env::var("REDIS_SENTINELS")
128 .context("REDIS_SENTINELS is required")?
129 .trim_matches('"')
130 .split(',')
131 .map(|s| s.to_string())
132 .collect(),
133 },
134 _ => {
135 return Err(anyhow::anyhow!(
136 "Invalid REDIS_MODE. Expected 'redis' or 'sentinel'."
137 ));
138 }
139 };
140
141 let app_debug_default = std::env::var("APP_DEBUG")
142 .unwrap_or("false".to_string())
143 .trim_matches('"')
144 .parse()
145 .context("Invalid APP_DEBUG value")?;
146
147 let app_encryption_key = std::env::var("APP_ENCRYPTION_KEY")
148 .expect("APP_ENCRYPTION_KEY is required")
149 .trim_matches('"')
150 .to_string();
151
152 if app_encryption_key.to_lowercase() == "changeme" {
153 println!(
154 "{}", "You are using the default APP_ENCRYPTION_KEY. This is unsupported, please modify your .env or your docker compose file.".red()
155 );
156 std::process::exit(1);
157 }
158
159 let app_log_directory = std::env::var("APP_LOG_DIRECTORY")
160 .ok()
161 .map(|s| s.trim_matches('"').to_string());
162
163 let (stdout_writer, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
164
165 let (appender, file_guard) = if let Some(app_log_directory) = &app_log_directory {
166 if !std::path::Path::new(app_log_directory).exists() {
167 std::fs::create_dir_all(app_log_directory)
168 .context("failed to create log directory")?;
169 }
170
171 let latest_log_path = std::path::Path::new(&app_log_directory).join("panel.log");
172 let latest_file = std::fs::OpenOptions::new()
173 .create(true)
174 .append(true)
175 .open(&latest_log_path)
176 .context("failed to open latest log file")?;
177
178 let rolling_appender = tracing_appender::rolling::Builder::new()
179 .filename_prefix("panel")
180 .filename_suffix("log")
181 .max_log_files(30)
182 .rotation(tracing_appender::rolling::Rotation::DAILY)
183 .build(app_log_directory)
184 .context("failed to create rolling log file appender")?;
185
186 let (appender, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
187 .buffered_lines_limit(50)
188 .finish(latest_file.and(rolling_appender));
189
190 (Some(appender), Some(guard))
191 } else {
192 (None, None)
193 };
194
195 let initial_filter = log_filter(app_debug_default);
196 let (reload_layer, log_reload_handle) =
197 tracing_subscriber::reload::Layer::new(initial_filter);
198
199 let fmt_layer = tracing_subscriber::fmt::layer()
200 .with_timer(tracing_subscriber::fmt::time::ChronoLocal::new(
201 "%Y-%m-%d %H:%M:%S %z".to_string(),
202 ))
203 .with_target(false)
204 .with_level(true)
205 .with_file(true)
206 .with_line_number(true);
207
208 let fmt_layer = if let Some(file_appender) = appender {
209 fmt_layer
210 .with_writer(stdout_writer.and(file_appender))
211 .boxed()
212 } else {
213 fmt_layer.with_writer(stdout_writer).boxed()
214 };
215
216 tracing_subscriber::registry()
217 .with(LevelFilter::DEBUG)
218 .with(reload_layer)
219 .with(fmt_layer)
220 .try_init()
221 .context("failed to install tracing subscriber")?;
222
223 let env = Self {
224 log_reload_handle,
225
226 redis_mode,
227
228 sentry_url: std::env::var("SENTRY_URL")
229 .ok()
230 .map(|s| s.trim_matches('"').to_string()),
231 sentry_tracing_sample_rate: std::env::var("SENTRY_TRACING_SAMPLE_RATE")
232 .unwrap_or("1.0".to_string())
233 .trim_matches('"')
234 .parse()
235 .context("Invalid SENTRY_TRACING_SAMPLE_RATE value")?,
236
237 database_migrate: std::env::var("DATABASE_MIGRATE")
238 .unwrap_or("false".to_string())
239 .trim_matches('"')
240 .parse()
241 .unwrap(),
242 database_url: std::env::var("DATABASE_URL")
243 .context("DATABASE_URL is required")?
244 .trim_matches('"')
245 .to_string(),
246 database_url_primary: std::env::var("DATABASE_URL_PRIMARY")
247 .ok()
248 .map(|s| s.trim_matches('"').to_string()),
249
250 bind: std::env::var("BIND")
251 .unwrap_or("0.0.0.0".to_string())
252 .trim_matches('"')
253 .to_string(),
254 port: std::env::var("PORT")
255 .unwrap_or("8000".to_string())
256 .parse()
257 .context("Invalid PORT value")?,
258
259 aio_base_wings_configuration: std::env::var("AIO_BASE_WINGS_CONFIGURATION")
260 .ok()
261 .map(|s| s.trim_matches('"').to_string()),
262
263 app_primary: std::env::var("APP_PRIMARY")
264 .unwrap_or("true".to_string())
265 .trim_matches('"')
266 .parse()
267 .context("Invalid APP_PRIMARY value")?,
268 app_debug_default,
269 app_debug: AtomicBool::new(app_debug_default),
270 app_enable_wings_proxy: std::env::var("APP_ENABLE_WINGS_PROXY")
271 .unwrap_or("false".to_string())
272 .trim_matches('"')
273 .parse()
274 .context("Invalid APP_ENABLE_WINGS_PROXY value")?,
275 app_disable_frontend: std::env::var("APP_DISABLE_FRONTEND")
276 .unwrap_or("false".to_string())
277 .trim_matches('"')
278 .parse()
279 .context("Invalid APP_DISABLE_FRONTEND value")?,
280 app_use_decryption_cache: std::env::var("APP_USE_DECRYPTION_CACHE")
281 .unwrap_or("false".to_string())
282 .trim_matches('"')
283 .parse()
284 .context("Invalid APP_USE_DECRYPTION_CACHE value")?,
285 app_use_internal_cache: std::env::var("APP_USE_INTERNAL_CACHE")
286 .unwrap_or("true".to_string())
287 .trim_matches('"')
288 .parse()
289 .context("Invalid APP_USE_INTERNAL_CACHE value")?,
290 app_trusted_proxies: std::env::var("APP_TRUSTED_PROXIES")
291 .unwrap_or("".to_string())
292 .trim_matches('"')
293 .split(',')
294 .filter_map(|s| if s.is_empty() { None } else { s.parse().ok() })
295 .collect(),
296 app_blocked_cidrs: match std::env::var("APP_BLOCKED_CIDRS") {
297 Ok(cidrs) => cidrs
298 .trim_matches('"')
299 .split(',')
300 .map(str::trim)
301 .filter(|s| !s.is_empty())
302 .map(str::parse)
303 .collect::<Result<_, _>>()
304 .context("Invalid APP_BLOCKED_CIDRS value")?,
305 Err(_) => default_blocked_cidrs(),
306 },
307 app_log_directory,
308 app_encryption_key,
309 server_name: std::env::var("SERVER_NAME")
310 .ok()
311 .map(|s| s.trim_matches('"').to_string()),
312 };
313
314 Ok((Arc::new(env), EnvGuard(file_guard, stdout_guard)))
315 }
316
317 #[inline]
318 pub fn find_ip(
319 &self,
320 headers: &HeaderMap,
321 connect_info: ConnectInfo<std::net::SocketAddr>,
322 ) -> std::net::IpAddr {
323 for cidr in &self.app_trusted_proxies {
324 if cidr.contains(&connect_info.ip()) {
325 if let Some(forwarded) = headers.get("X-Forwarded-For")
326 && let Ok(forwarded) = forwarded.to_str()
327 && let Some(ip) = forwarded.split(',').next()
328 {
329 return ip.parse().unwrap_or_else(|_| connect_info.ip());
330 }
331
332 if let Some(forwarded) = headers.get("X-Real-IP")
333 && let Ok(forwarded) = forwarded.to_str()
334 {
335 return forwarded.parse().unwrap_or_else(|_| connect_info.ip());
336 }
337 }
338 }
339
340 connect_info.ip()
341 }
342
343 #[inline]
344 pub fn is_debug(&self) -> bool {
345 self.app_debug.load(std::sync::atomic::Ordering::Relaxed)
346 }
347
348 pub fn set_debug(&self, debug: bool) -> Result<(), anyhow::Error> {
349 self.app_debug
350 .store(debug, std::sync::atomic::Ordering::Relaxed);
351
352 self.log_reload_handle
353 .modify(|filter| *filter = log_filter(debug))
354 .context("failed to reload tracing level filter")?;
355
356 Ok(())
357 }
358}