Skip to main content

gephyr_lib/
lib.rs

1mod commands;
2pub mod constants;
3pub mod error;
4mod models;
5mod modules;
6mod proxy;
7#[cfg(test)]
8mod test_utils;
9mod utils;
10
11use modules::system::logger;
12use tracing::{error, info, warn};
13#[cfg(target_os = "macos")]
14fn increase_nofile_limit() {
15    unsafe {
16        let mut rl = libc::rlimit {
17            rlim_cur: 0,
18            rlim_max: 0,
19        };
20
21        if libc::getrlimit(libc::RLIMIT_NOFILE, &mut rl) == 0 {
22            info!(
23                "Current open file limit: soft={}, hard={}",
24                rl.rlim_cur, rl.rlim_max
25            );
26            let target = 4096.min(rl.rlim_max);
27            if rl.rlim_cur < target {
28                rl.rlim_cur = target;
29                if libc::setrlimit(libc::RLIMIT_NOFILE, &rl) == 0 {
30                    info!("Successfully increased hard file limit to {}", target);
31                } else {
32                    warn!("[W-RUNTIME-NOFILE-LIMIT] failed_to_increase_file_descriptor_limit");
33                }
34            }
35        }
36    }
37}
38
39fn parse_env_bool(value: &str) -> Option<bool> {
40    match value.trim().to_ascii_lowercase().as_str() {
41        "1" | "true" | "yes" | "on" => Some(true),
42        "0" | "false" | "no" | "off" => Some(false),
43        _ => None,
44    }
45}
46
47fn parse_auth_mode(value: &str) -> Option<crate::proxy::ProxyAuthMode> {
48    match value.trim().to_ascii_lowercase().as_str() {
49        "off" => Some(crate::proxy::ProxyAuthMode::Off),
50        "strict" => Some(crate::proxy::ProxyAuthMode::Strict),
51        "all_except_health" => Some(crate::proxy::ProxyAuthMode::AllExceptHealth),
52        _ => None,
53    }
54}
55
56fn apply_headless_env_overrides(config: &mut crate::models::AppConfig) {
57    if let Ok(key) = std::env::var("API_KEY") {
58        if !key.trim().is_empty() {
59            info!("Using API key from environment");
60            config.proxy.api_key = key;
61        }
62    }
63
64    if let Ok(port) = std::env::var("PORT") {
65        let trimmed = port.trim();
66        if !trimmed.is_empty() {
67            match trimmed.parse::<u16>() {
68                Ok(p) if p > 0 => {
69                    config.proxy.port = p;
70                    info!("Using proxy port from environment: {}", p);
71                }
72                _ => warn!("[W-PORT-INVALID] ignoring_invalid_port_value: {}", port),
73            }
74        }
75    }
76
77    if let Ok(password) = std::env::var("WEB_PASSWORD") {
78        if !password.trim().is_empty() {
79            info!("Using web admin password from environment");
80            config.proxy.admin_password = Some(password);
81        }
82    }
83
84    if let Ok(mode) = std::env::var("AUTH_MODE") {
85        if mode.trim().eq_ignore_ascii_case("auto") {
86            warn!(
87                "[W-AUTH-MODE-AUTO-DEPRECATED] auth_mode_auto_is_deprecated_coercing_to_strict_in_headless_mode"
88            );
89            config.proxy.auth_mode = crate::proxy::ProxyAuthMode::Strict;
90        } else {
91            match parse_auth_mode(&mode) {
92                Some(parsed) => {
93                    info!("Using auth mode from environment: {:?}", parsed);
94                    config.proxy.auth_mode = parsed;
95                }
96                None => warn!(
97                    "[W-AUTH-MODE-INVALID] ignoring_invalid_auth_mode_value: {}",
98                    mode
99                ),
100            }
101        }
102    }
103
104    if let Ok(allow_lan) = std::env::var("ALLOW_LAN_ACCESS") {
105        if let Some(parsed) = parse_env_bool(&allow_lan) {
106            config.proxy.allow_lan_access = parsed;
107            info!(
108                "Using LAN access setting from environment: {}",
109                config.proxy.allow_lan_access
110            );
111        } else {
112            warn!(
113                "[W-LAN-ACCESS-INVALID] ignoring_invalid_lan_access_value: {}",
114                allow_lan
115            );
116        }
117    }
118}
119
120fn apply_security_hardening(config: &mut crate::models::AppConfig) {
121    if matches!(config.proxy.auth_mode, crate::proxy::ProxyAuthMode::Off) {
122        warn!("[W-AUTH-MODE-HARDENED] auth_mode_off_forcing_strict_in_headless_mode");
123        config.proxy.auth_mode = crate::proxy::ProxyAuthMode::Strict;
124    }
125}
126
127async fn start_headless_runtime() -> Result<commands::proxy::ProxyServiceState, String> {
128    let proxy_state = commands::proxy::ProxyServiceState::new();
129    crate::utils::crypto::validate_encryption_key_prerequisites().map_err(|e| {
130        format!(
131            "ERROR [E-CRYPTO-KEY-UNAVAILABLE] startup_encryption_preflight_failed: {} Refusing to start because encrypted token/config operations would fail at runtime. In Docker/container environments machine UID may be unavailable. Remediation: set ENCRYPTION_KEY, restart Gephyr, then rerun OAuth login.",
132            e
133        )
134    })?;
135    if let Err(e) = crate::modules::auth::account::startup_preflight_verify_persisted_tokens() {
136        return Err(format!(
137            "ERROR [E-CRYPTO-KEY-MISMATCH] startup_token_decryption_preflight_failed: {} Remediation: ensure ENCRYPTION_KEY matches the key used when these tokens were stored, or run --reencrypt-secrets with the correct key.",
138            e
139        ));
140    }
141    let mut config = modules::system::config::load_app_config()
142        .map_err(|e| format!("failed_to_load_config_for_headless_mode: {}", e))?;
143
144    apply_headless_env_overrides(&mut config);
145    apply_security_hardening(&mut config);
146    modules::system::validation::validate_app_config(&config).map_err(|errors| {
147        format!(
148            "configuration_validation_failed:\n{}",
149            errors
150                .iter()
151                .map(|e| e.to_string())
152                .collect::<Vec<_>>()
153                .join("\n")
154        )
155    })?;
156    crate::utils::http::log_tls_startup_diagnostics();
157    if let Err(e) = crate::utils::http::run_tls_startup_canary_probe().await {
158        if crate::utils::http::tls_canary_required() {
159            return Err(format!(
160                "ERROR [E-TLS-CANARY-REQUIRED] tls_startup_canary_probe_failed: {}",
161                e
162            ));
163        }
164        warn!("[W-TLS-CANARY-FAILED] {}", e);
165    }
166
167    info!(
168        "Starting headless proxy service on port {}",
169        config.proxy.port
170    );
171    if config.proxy.allow_lan_access {
172        warn!("[W-LAN-ACCESS-ENABLED] lan_access_enabled_bind_address_0_0_0_0");
173    } else {
174        info!("LAN access is disabled (bind address will be 127.0.0.1)");
175    }
176
177    commands::proxy::internal_start_proxy_service(
178        config.proxy,
179        &proxy_state,
180        crate::modules::system::integration::SystemManager::Headless,
181    )
182    .await
183    .map_err(|e| format!("failed_to_start_headless_proxy_service: {}", e))?;
184
185    modules::system::scheduler::start_scheduler(proxy_state.clone());
186    info!("Headless scheduler started");
187    Ok(proxy_state)
188}
189
190pub fn run() {
191    #[cfg(target_os = "macos")]
192    increase_nofile_limit();
193
194    logger::init_logger();
195    crate::utils::crypto::warn_if_weak_encryption_key();
196
197    if let Err(e) = modules::stats::token_stats::init_db() {
198        error!(
199            "[E-DB-TOKEN-STATS-INIT] failed_to_initialize_token_stats_database: {}",
200            e
201        );
202    }
203    if let Err(e) = modules::persistence::security_db::init_db() {
204        error!(
205            "[E-DB-SECURITY-INIT] failed_to_initialize_security_database: {}",
206            e
207        );
208    }
209    if let Err(e) = modules::persistence::user_token_db::init_db() {
210        error!(
211            "[E-DB-USER-TOKEN-INIT] failed_to_initialize_user_token_database: {}",
212            e
213        );
214    }
215
216    let args: Vec<String> = std::env::args().collect();
217    if args.iter().any(|arg| arg == "--reencrypt-secrets") {
218        info!("Running one-time secret re-encryption utility");
219        match commands::crypto::reencrypt_all_secrets() {
220            Ok(report) => {
221                info!(
222                    "Secret re-encryption completed: config_rewritten={}, accounts_total={}, accounts_rewritten={}, accounts_failed={}",
223                    report.config_rewritten,
224                    report.accounts_total,
225                    report.accounts_rewritten,
226                    report.accounts_failed
227                );
228                return;
229            }
230            Err(e) => {
231                error!("[E-SECRET-REENCRYPT] secret_reencryption_failed: {}", e);
232                std::process::exit(1);
233            }
234        }
235    }
236
237    if !args.iter().any(|arg| arg == "--headless") {
238        warn!("[W-RUNTIME-HEADLESS-DEFAULT] starting_headless_runtime_headless_flag_is_optional");
239    }
240
241    let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
242    runtime.block_on(async {
243        let proxy_state = match start_headless_runtime().await {
244            Ok(state) => state,
245            Err(e) => {
246                error!("[E-RUNTIME-STARTUP] {}", e);
247                std::process::exit(1);
248            }
249        };
250
251        info!("Headless service is running. Press Ctrl+C to exit.");
252        let _ = tokio::signal::ctrl_c().await;
253        info!("Shutting down headless service");
254        if let Err(e) = commands::proxy::internal_stop_proxy_service(&proxy_state).await {
255            warn!(
256                "[W-RUNTIME-STOP] failed_to_stop_proxy_service_cleanly: {}",
257                e
258            );
259        }
260    });
261}
262
263#[cfg(test)]
264mod tests {
265    use super::{apply_headless_env_overrides, apply_security_hardening, parse_auth_mode};
266    use crate::models::AppConfig;
267    use crate::proxy::ProxyAuthMode;
268    use crate::test_utils::ScopedEnvVar;
269    use std::sync::{Mutex, OnceLock};
270
271    static LIB_TEST_ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
272
273    #[test]
274    fn parse_auth_mode_rejects_auto() {
275        assert!(parse_auth_mode("auto").is_none());
276    }
277
278    #[test]
279    fn headless_env_auto_auth_mode_is_coerced_to_strict() {
280        let _guard = LIB_TEST_ENV_LOCK
281            .get_or_init(|| Mutex::new(()))
282            .lock()
283            .expect("lib env test lock");
284        let _auth_mode = ScopedEnvVar::set("AUTH_MODE", "auto");
285
286        let mut config = AppConfig::default();
287        config.proxy.auth_mode = ProxyAuthMode::AllExceptHealth;
288
289        apply_headless_env_overrides(&mut config);
290        apply_security_hardening(&mut config);
291
292        assert!(matches!(config.proxy.auth_mode, ProxyAuthMode::Strict));
293    }
294
295    #[test]
296    fn headless_env_port_overrides_config_port() {
297        let _guard = LIB_TEST_ENV_LOCK
298            .get_or_init(|| Mutex::new(()))
299            .lock()
300            .expect("lib env test lock");
301        let _port = ScopedEnvVar::set("PORT", "8045");
302
303        let mut config = AppConfig::default();
304        config.proxy.port = 8145;
305        apply_headless_env_overrides(&mut config);
306
307        assert_eq!(config.proxy.port, 8045);
308    }
309}