use std::time::Duration; use tokio::sync::watch; use tracing::{debug, error, info, warn}; use crate::config::ProxyConfig; use crate::transport::UpstreamManager; use crate::transport::middle_proxy::{ ProxyConfigData, fetch_proxy_config_with_raw_via_upstream, load_proxy_config_cache, save_proxy_config_cache, }; use super::print_maestro_line; pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) { print_maestro_line(format!("Proxy links ({host})")); for user_name in config .general .links .show .resolve_users(&config.access.users) { if let Some(secret) = config.access.users.get(user_name) { print_maestro_line(format!("User: {user_name}")); if config.general.modes.classic { print_maestro_line(format!( "Classic: tg://proxy?server={host}&port={port}&secret={secret}" )); } if config.general.modes.secure { print_maestro_line(format!( "DD: tg://proxy?server={host}&port={port}&secret=dd{secret}" )); } if config.general.modes.tls { let mut domains = Vec::with_capacity(1 + config.censorship.tls_domains.len()); domains.push(config.censorship.tls_domain.clone()); for d in &config.censorship.tls_domains { if !domains.contains(d) { domains.push(d.clone()); } } for domain in domains { let domain_hex = hex::encode(&domain); print_maestro_line(format!( "EE-TLS: tg://proxy?server={host}&port={port}&secret=ee{secret}{domain_hex}" )); } } } else { warn!(target: "telemt::links", "User '{}' in show_link not found", user_name); } } } /// Prints WEB links only for profiles selected by the existing link policy. pub(crate) fn print_web_proxy_links(config: &ProxyConfig) { if !config.web.enabled || config.general.links.show.is_empty() { return; } let Some(runtime) = config.web.runtime.as_ref() else { return; }; let shown = config .general .links .show .resolve_users(&config.access.users); let mut heading_printed = false; for profile in &runtime.profiles { if !shown.iter().any(|user| user.as_str() == profile.user) { continue; } if !heading_printed { print_maestro_line("WEB proxy links"); heading_printed = true; } let Some(secret) = config.access.users.get(&profile.user) else { continue; }; let prefix = match profile.secret_mode { crate::config::WebSecretMode::Plain => "", crate::config::WebSecretMode::Dd => "dd", }; print_maestro_line(format!( "User: {} ({:?})", profile.user, profile.secret_mode )); print_maestro_line(format!( "WEB: tg://webproxy?server={}&secret={prefix}{secret}", profile.host, )); } } pub(crate) async fn write_beobachten_snapshot(path: &str, payload: &str) -> std::io::Result<()> { #[cfg(unix)] { crate::util::secure_fs::atomic_replace_async( std::path::PathBuf::from(path), payload.as_bytes().to_vec(), 0o600, ) .await } #[cfg(not(unix))] { if let Some(parent) = std::path::Path::new(path).parent() && !parent.as_os_str().is_empty() { tokio::fs::create_dir_all(parent).await?; } tokio::fs::write(path, payload).await } } pub(crate) fn unit_label(value: u64, singular: &'static str, plural: &'static str) -> &'static str { if value == 1 { singular } else { plural } } pub(crate) fn format_uptime(total_secs: u64) -> String { const SECS_PER_MINUTE: u64 = 60; const SECS_PER_HOUR: u64 = 60 * SECS_PER_MINUTE; const SECS_PER_DAY: u64 = 24 * SECS_PER_HOUR; const SECS_PER_MONTH: u64 = 30 * SECS_PER_DAY; const SECS_PER_YEAR: u64 = 12 * SECS_PER_MONTH; let mut remaining = total_secs; let years = remaining / SECS_PER_YEAR; remaining %= SECS_PER_YEAR; let months = remaining / SECS_PER_MONTH; remaining %= SECS_PER_MONTH; let days = remaining / SECS_PER_DAY; remaining %= SECS_PER_DAY; let hours = remaining / SECS_PER_HOUR; remaining %= SECS_PER_HOUR; let minutes = remaining / SECS_PER_MINUTE; let seconds = remaining % SECS_PER_MINUTE; let mut parts = Vec::new(); if total_secs > SECS_PER_YEAR { parts.push(format!("{} {}", years, unit_label(years, "year", "years"))); } if total_secs > SECS_PER_MONTH { parts.push(format!( "{} {}", months, unit_label(months, "month", "months") )); } if total_secs > SECS_PER_DAY { parts.push(format!("{} {}", days, unit_label(days, "day", "days"))); } if total_secs > SECS_PER_HOUR { parts.push(format!("{} {}", hours, unit_label(hours, "hour", "hours"))); } if total_secs > SECS_PER_MINUTE { parts.push(format!( "{} {}", minutes, unit_label(minutes, "minute", "minutes") )); } parts.push(format!( "{} {}", seconds, unit_label(seconds, "second", "seconds") )); format!("{} / {} seconds", parts.join(", "), total_secs) } #[allow(dead_code)] pub(crate) async fn wait_until_admission_open(admission_rx: &mut watch::Receiver) -> bool { loop { if *admission_rx.borrow() { return true; } if admission_rx.changed().await.is_err() { return *admission_rx.borrow(); } } } pub(crate) fn is_expected_handshake_eof(err: &crate::error::ProxyError) -> bool { expected_handshake_close_description(err).is_some() } pub(crate) fn peer_close_description(err: &crate::error::ProxyError) -> Option<&'static str> { fn from_kind(kind: std::io::ErrorKind) -> Option<&'static str> { match kind { std::io::ErrorKind::ConnectionReset => Some("Peer reset TCP connection (RST)"), std::io::ErrorKind::ConnectionAborted => { Some("Peer aborted TCP connection during transport") } std::io::ErrorKind::BrokenPipe => Some("Peer closed write side (broken pipe)"), std::io::ErrorKind::NotConnected => Some("Socket was already closed by peer"), _ => None, } } match err { crate::error::ProxyError::Io(ioe) => from_kind(ioe.kind()), crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) => { from_kind(ioe.kind()) } _ => None, } } pub(crate) fn expected_handshake_close_description( err: &crate::error::ProxyError, ) -> Option<&'static str> { fn from_kind(kind: std::io::ErrorKind) -> Option<&'static str> { match kind { std::io::ErrorKind::UnexpectedEof => { Some("Peer closed before sending full 64-byte MTProto handshake") } std::io::ErrorKind::ConnectionReset => { Some("Peer reset TCP connection during initial MTProto handshake") } std::io::ErrorKind::ConnectionAborted => { Some("Peer aborted TCP connection during initial MTProto handshake") } std::io::ErrorKind::BrokenPipe => { Some("Peer closed write side before MTProto handshake completed") } std::io::ErrorKind::NotConnected => Some("Handshake socket was already closed by peer"), _ => None, } } match err { crate::error::ProxyError::Io(ioe) => from_kind(ioe.kind()), crate::error::ProxyError::Stream(crate::error::StreamError::UnexpectedEof) => { Some("Peer closed before sending full 64-byte MTProto handshake") } crate::error::ProxyError::Stream(crate::error::StreamError::Io(ioe)) => { from_kind(ioe.kind()) } _ => None, } } pub(crate) async fn load_startup_proxy_config_snapshot( url: &str, cache_path: Option<&str>, me2dc_fallback: bool, label: &'static str, upstream: Option>, ) -> Option { loop { match fetch_proxy_config_with_raw_via_upstream(url, upstream.clone()).await { Ok((cfg, raw)) => { if !cfg.map.is_empty() { if let Some(path) = cache_path && let Err(e) = save_proxy_config_cache(path, &raw).await { warn!(error = %e, path, snapshot = label, "Failed to store startup proxy-config cache"); } return Some(cfg); } warn!( snapshot = label, url, "Startup proxy-config is empty; trying disk cache" ); if let Some(path) = cache_path { match load_proxy_config_cache(path).await { Ok(cached) if !cached.map.is_empty() => { info!( snapshot = label, path, proxy_for_lines = cached.proxy_for_lines, "Loaded startup proxy-config from disk cache" ); return Some(cached); } Ok(_) => { warn!( snapshot = label, path, "Startup proxy-config cache is empty; ignoring cache file" ); } Err(cache_err) => { debug!( snapshot = label, path, error = %cache_err, "Startup proxy-config cache unavailable" ); } } } if me2dc_fallback { error!( snapshot = label, "Startup proxy-config unavailable and no saved config found; falling back to direct mode" ); return None; } warn!( snapshot = label, retry_in_secs = 2, "Startup proxy-config unavailable and no saved config found; retrying because me2dc_fallback=false" ); tokio::time::sleep(Duration::from_secs(2)).await; } Err(fetch_err) => { if let Some(path) = cache_path { match load_proxy_config_cache(path).await { Ok(cached) if !cached.map.is_empty() => { info!( snapshot = label, path, proxy_for_lines = cached.proxy_for_lines, "Loaded startup proxy-config from disk cache" ); return Some(cached); } Ok(_) => { warn!( snapshot = label, path, "Startup proxy-config cache is empty; ignoring cache file" ); } Err(cache_err) => { debug!( snapshot = label, path, error = %cache_err, "Startup proxy-config cache unavailable" ); } } } if me2dc_fallback { error!( snapshot = label, error = %fetch_err, "Startup proxy-config unavailable and no cached data; falling back to direct mode" ); return None; } warn!( snapshot = label, error = %fetch_err, retry_in_secs = 2, "Startup proxy-config unavailable; retrying because me2dc_fallback=false" ); tokio::time::sleep(Duration::from_secs(2)).await; } } } }