mirror of
https://github.com/telemt/telemt.git
synced 2026-10-07 09:55:57 +03:00
361 lines
13 KiB
Rust
361 lines
13 KiB
Rust
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>) -> 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<std::sync::Arc<UpstreamManager>>,
|
|
) -> Option<ProxyConfigData> {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|