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