Skip to main content

shared/
env.rs

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}