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}