1use anyhow::Context;
2use axum::{extract::ConnectInfo, http::HeaderMap};
3use colored::Colorize;
4use dotenvy::dotenv;
5use std::{
6 io::{BufWriter, IsTerminal},
7 sync::{Arc, atomic::AtomicBool},
8};
9use tracing_subscriber::{
10 Layer,
11 filter::{LevelFilter, Targets},
12 fmt::writer::MakeWriterExt,
13 layer::{Layered, SubscriberExt},
14 util::SubscriberInitExt,
15};
16
17#[derive(Clone)]
18pub enum RedisMode {
19 Redis {
20 redis_url: Option<String>,
21 },
22 Sentinel {
23 cluster_name: String,
24 redis_sentinels: Vec<String>,
25 },
26}
27
28impl std::fmt::Display for RedisMode {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 RedisMode::Redis { .. } => write!(f, "Redis"),
32 RedisMode::Sentinel { .. } => write!(f, "Sentinel"),
33 }
34 }
35}
36
37pub struct EnvGuard(
38 pub Option<tracing_appender::non_blocking::WorkerGuard>,
39 pub tracing_appender::non_blocking::WorkerGuard,
40);
41
42type ReloadHandle =
43 tracing_subscriber::reload::Handle<Targets, Layered<LevelFilter, tracing_subscriber::Registry>>;
44
45const LOG_CHANNEL_LINES: usize = 4096;
46
47fn log_filter(debug: bool) -> Targets {
48 let crate_level = if debug {
49 LevelFilter::DEBUG
50 } else {
51 LevelFilter::INFO
52 };
53
54 Targets::new()
55 .with_default(LevelFilter::INFO)
56 .with_target("backend", crate_level)
57 .with_target("database_migrator", crate_level)
58 .with_target("shared", crate_level)
59 .with_target("panel_rs", crate_level)
60 .with_target("panel_rs_aio", crate_level)
61}
62
63fn default_blocked_cidrs() -> Vec<cidr::IpCidr> {
64 const DEFAULTS: [&str; 17] = [
65 "0.0.0.0/8",
66 "127.0.0.0/8",
67 "10.0.0.0/8",
68 "100.64.0.0/10",
69 "172.16.0.0/12",
70 "192.168.0.0/16",
71 "169.254.0.0/16",
72 "192.0.0.0/24",
73 "198.18.0.0/15",
74 "224.0.0.0/4",
75 "240.0.0.0/4",
76 "::/128",
77 "::1/128",
78 "fe80::/10",
79 "fc00::/7",
80 "2002::/16",
81 "ff00::/8",
82 ];
83
84 DEFAULTS
85 .iter()
86 .map(|cidr| cidr.parse().expect("invalid default blocked cidr"))
87 .collect()
88}
89
90pub struct Env {
91 log_reload_handle: ReloadHandle,
92
93 pub redis_mode: RedisMode,
94 pub redis_key_prefix: Option<String>,
95
96 pub sentry_url: Option<String>,
97 pub sentry_tracing_sample_rate: f32,
98
99 pub database_migrate: bool,
100 pub database_url: String,
101 pub database_url_primary: Option<String>,
102
103 pub bind: String,
104 pub port: u16,
105
106 pub aio_base_wings_configuration: Option<String>,
107
108 pub app_primary: bool,
109 pub app_debug_default: bool,
110 app_debug: AtomicBool,
111 pub app_enable_wings_proxy: bool,
112 pub app_disable_frontend: bool,
113 pub app_disable_extension_management: bool,
114 pub app_use_decryption_cache: bool,
115 pub app_use_internal_cache: bool,
116 pub app_trusted_proxies: Vec<cidr::IpCidr>,
117 pub app_blocked_cidrs: Vec<cidr::IpCidr>,
118 pub app_log_directory: Option<String>,
119 pub app_request_log_limit: usize,
120 pub app_encryption_key: String,
121 pub server_name: Option<String>,
122}
123
124impl Env {
125 pub fn parse() -> Result<(Arc<Self>, EnvGuard), anyhow::Error> {
126 dotenv().ok();
127
128 let redis_mode = match std::env::var("REDIS_MODE")
129 .unwrap_or("redis".to_string())
130 .trim_matches('"')
131 {
132 "redis" => RedisMode::Redis {
133 redis_url: std::env::var("REDIS_URL")
134 .ok()
135 .map(|s| s.trim_matches('"').to_string()),
136 },
137 "sentinel" => RedisMode::Sentinel {
138 cluster_name: std::env::var("REDIS_SENTINEL_CLUSTER")
139 .context("REDIS_SENTINEL_CLUSTER is required")?
140 .trim_matches('"')
141 .to_string(),
142 redis_sentinels: std::env::var("REDIS_SENTINELS")
143 .context("REDIS_SENTINELS is required")?
144 .trim_matches('"')
145 .split(',')
146 .map(|s| s.to_string())
147 .collect(),
148 },
149 _ => {
150 return Err(anyhow::anyhow!(
151 "Invalid REDIS_MODE. Expected 'redis' or 'sentinel'."
152 ));
153 }
154 };
155
156 let app_debug_default = std::env::var("APP_DEBUG")
157 .unwrap_or("false".to_string())
158 .trim_matches('"')
159 .parse()
160 .context("Invalid APP_DEBUG value")?;
161
162 let app_encryption_key = std::env::var("APP_ENCRYPTION_KEY")
163 .expect("APP_ENCRYPTION_KEY is required")
164 .trim_matches('"')
165 .to_string();
166
167 if app_encryption_key.to_lowercase() == "changeme" {
168 println!(
169 "{}", "You are using the default APP_ENCRYPTION_KEY. This is unsupported, please modify your .env or your docker compose file.".red()
170 );
171 std::process::exit(1);
172 }
173
174 let app_log_directory = std::env::var("APP_LOG_DIRECTORY")
175 .ok()
176 .map(|s| s.trim_matches('"').to_string());
177
178 let (stdout_writer, stdout_guard) =
179 tracing_appender::non_blocking::NonBlockingBuilder::default()
180 .buffered_lines_limit(LOG_CHANNEL_LINES)
181 .finish(BufWriter::new(std::io::stdout()));
182
183 let (appender, file_guard) = if let Some(app_log_directory) = &app_log_directory {
184 if !std::path::Path::new(app_log_directory).exists() {
185 std::fs::create_dir_all(app_log_directory)
186 .context("failed to create log directory")?;
187 }
188
189 let latest_log_path = std::path::Path::new(&app_log_directory).join("panel.log");
190 let latest_file = std::fs::OpenOptions::new()
191 .create(true)
192 .append(true)
193 .open(&latest_log_path)
194 .context("failed to open latest log file")?;
195
196 let rolling_appender = tracing_appender::rolling::Builder::new()
197 .filename_prefix("panel")
198 .filename_suffix("log")
199 .max_log_files(30)
200 .rotation(tracing_appender::rolling::Rotation::DAILY)
201 .build(app_log_directory)
202 .context("failed to create rolling log file appender")?;
203
204 let (appender, guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
205 .buffered_lines_limit(LOG_CHANNEL_LINES)
206 .finish(BufWriter::new(latest_file.and(rolling_appender)));
207
208 (Some(appender), Some(guard))
209 } else {
210 (None, None)
211 };
212
213 let initial_filter = log_filter(app_debug_default);
214 let (reload_layer, log_reload_handle) =
215 tracing_subscriber::reload::Layer::new(initial_filter);
216
217 let fmt_layer = tracing_subscriber::fmt::layer()
218 .with_timer(tracing_subscriber::fmt::time::ChronoLocal::new(
219 "%Y-%m-%d %H:%M:%S %z".to_string(),
220 ))
221 .with_ansi(std::io::stdout().is_terminal())
222 .with_target(false)
223 .with_level(true)
224 .with_file(true)
225 .with_line_number(true);
226
227 let fmt_layer = if let Some(file_appender) = appender {
228 fmt_layer
229 .with_writer(stdout_writer.and(file_appender))
230 .boxed()
231 } else {
232 fmt_layer.with_writer(stdout_writer).boxed()
233 };
234
235 tracing_subscriber::registry()
236 .with(LevelFilter::DEBUG)
237 .with(reload_layer)
238 .with(fmt_layer)
239 .with(sentry_tracing::layer().enable_span_attributes())
240 .try_init()
241 .context("failed to install tracing subscriber")?;
242
243 let env = Self {
244 log_reload_handle,
245
246 redis_mode,
247 redis_key_prefix: std::env::var("REDIS_KEY_PREFIX")
248 .ok()
249 .map(|s| s.trim_matches('"').trim_end_matches(':').to_string())
250 .filter(|s| !s.is_empty()),
251
252 sentry_url: std::env::var("SENTRY_URL")
253 .ok()
254 .map(|s| s.trim_matches('"').to_string()),
255 sentry_tracing_sample_rate: std::env::var("SENTRY_TRACING_SAMPLE_RATE")
256 .unwrap_or("1.0".to_string())
257 .trim_matches('"')
258 .parse()
259 .context("Invalid SENTRY_TRACING_SAMPLE_RATE value")?,
260
261 database_migrate: std::env::var("DATABASE_MIGRATE")
262 .unwrap_or("false".to_string())
263 .trim_matches('"')
264 .parse()
265 .unwrap(),
266 database_url: std::env::var("DATABASE_URL")
267 .context("DATABASE_URL is required")?
268 .trim_matches('"')
269 .to_string(),
270 database_url_primary: std::env::var("DATABASE_URL_PRIMARY")
271 .ok()
272 .map(|s| s.trim_matches('"').to_string()),
273
274 bind: std::env::var("BIND")
275 .unwrap_or("0.0.0.0".to_string())
276 .trim_matches('"')
277 .to_string(),
278 port: std::env::var("PORT")
279 .unwrap_or("8000".to_string())
280 .parse()
281 .context("Invalid PORT value")?,
282
283 aio_base_wings_configuration: std::env::var("AIO_BASE_WINGS_CONFIGURATION")
284 .ok()
285 .map(|s| s.trim_matches('"').to_string()),
286
287 app_primary: std::env::var("APP_PRIMARY")
288 .unwrap_or("true".to_string())
289 .trim_matches('"')
290 .parse()
291 .context("Invalid APP_PRIMARY value")?,
292 app_debug_default,
293 app_debug: AtomicBool::new(app_debug_default),
294 app_enable_wings_proxy: std::env::var("APP_ENABLE_WINGS_PROXY")
295 .unwrap_or("false".to_string())
296 .trim_matches('"')
297 .parse()
298 .context("Invalid APP_ENABLE_WINGS_PROXY value")?,
299 app_disable_frontend: std::env::var("APP_DISABLE_FRONTEND")
300 .unwrap_or("false".to_string())
301 .trim_matches('"')
302 .parse()
303 .context("Invalid APP_DISABLE_FRONTEND value")?,
304 app_disable_extension_management: std::env::var("APP_DISABLE_EXTENSION_MANAGEMENT")
305 .unwrap_or("false".to_string())
306 .trim_matches('"')
307 .parse()
308 .context("Invalid APP_DISABLE_EXTENSION_MANAGEMENT value")?,
309 app_use_decryption_cache: std::env::var("APP_USE_DECRYPTION_CACHE")
310 .unwrap_or("false".to_string())
311 .trim_matches('"')
312 .parse()
313 .context("Invalid APP_USE_DECRYPTION_CACHE value")?,
314 app_use_internal_cache: std::env::var("APP_USE_INTERNAL_CACHE")
315 .unwrap_or("true".to_string())
316 .trim_matches('"')
317 .parse()
318 .context("Invalid APP_USE_INTERNAL_CACHE value")?,
319 app_trusted_proxies: std::env::var("APP_TRUSTED_PROXIES")
320 .unwrap_or("".to_string())
321 .trim_matches('"')
322 .split(',')
323 .filter_map(|s| if s.is_empty() { None } else { s.parse().ok() })
324 .collect(),
325 app_blocked_cidrs: match std::env::var("APP_BLOCKED_CIDRS") {
326 Ok(cidrs) => cidrs
327 .trim_matches('"')
328 .split(',')
329 .map(str::trim)
330 .filter(|s| !s.is_empty())
331 .map(str::parse)
332 .collect::<Result<_, _>>()
333 .context("Invalid APP_BLOCKED_CIDRS value")?,
334 Err(_) => default_blocked_cidrs(),
335 },
336 app_log_directory,
337 app_request_log_limit: std::env::var("APP_REQUEST_LOG_LIMIT")
338 .unwrap_or("250".to_string())
339 .trim_matches('"')
340 .parse()
341 .context("Invalid APP_REQUEST_LOG_LIMIT value")?,
342 app_encryption_key,
343 server_name: std::env::var("SERVER_NAME")
344 .ok()
345 .map(|s| s.trim_matches('"').to_string()),
346 };
347
348 Ok((Arc::new(env), EnvGuard(file_guard, stdout_guard)))
349 }
350
351 #[inline]
352 pub fn find_ip(
353 &self,
354 headers: &HeaderMap,
355 connect_info: ConnectInfo<std::net::SocketAddr>,
356 ) -> std::net::IpAddr {
357 let peer = connect_info.ip();
358
359 if !self
360 .app_trusted_proxies
361 .iter()
362 .any(|cidr| cidr.contains(&peer))
363 {
364 return peer;
365 }
366
367 fn find_forwarded_ip(
368 forwarded: &str,
369 trusted_proxies: &[cidr::IpCidr],
370 ) -> Option<std::net::IpAddr> {
371 for entry in forwarded.rsplit(',') {
372 let ip: std::net::IpAddr = entry.trim().parse().ok()?;
373
374 if !trusted_proxies.iter().any(|cidr| cidr.contains(&ip)) {
375 return Some(ip);
376 }
377 }
378
379 None
380 }
381
382 if let Some(forwarded) = headers.get("X-Forwarded-For")
383 && let Ok(forwarded) = forwarded.to_str()
384 && let Some(ip) = find_forwarded_ip(forwarded, &self.app_trusted_proxies)
385 {
386 return ip;
387 }
388
389 if let Some(forwarded) = headers.get("X-Real-IP")
390 && let Ok(forwarded) = forwarded.to_str()
391 && let Ok(ip) = forwarded.trim().parse()
392 {
393 return ip;
394 }
395
396 peer
397 }
398
399 pub fn find_host(
400 &self,
401 headers: &HeaderMap,
402 uri: &axum::http::Uri,
403 connect_info: ConnectInfo<std::net::SocketAddr>,
404 ) -> Option<compact_str::CompactString> {
405 let peer = connect_info.ip();
406
407 if self
408 .app_trusted_proxies
409 .iter()
410 .any(|cidr| cidr.contains(&peer))
411 && let Some(forwarded) = headers.get("X-Forwarded-Host")
412 && let Ok(forwarded) = forwarded.to_str()
413 && let Some(host) = forwarded.split(',').next()
414 && !host.trim().is_empty()
415 {
416 return Some(compact_str::CompactString::from(host.trim()).to_ascii_lowercase());
417 }
418
419 headers
420 .get(axum::http::header::HOST)
421 .and_then(|host| host.to_str().ok())
422 .or_else(|| uri.authority().map(|authority| authority.as_str()))
423 .map(|host| compact_str::CompactString::from(host.trim()).to_ascii_lowercase())
424 }
425
426 #[inline]
427 pub fn is_debug(&self) -> bool {
428 self.app_debug.load(std::sync::atomic::Ordering::Relaxed)
429 }
430
431 pub fn set_debug(&self, debug: bool) -> Result<(), anyhow::Error> {
432 self.app_debug
433 .store(debug, std::sync::atomic::Ordering::Relaxed);
434
435 self.log_reload_handle
436 .modify(|filter| *filter = log_filter(debug))
437 .context("failed to reload tracing level filter")?;
438
439 Ok(())
440 }
441}