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}