mirror of
https://github.com/telemt/telemt.git
synced 2026-10-07 09:55:57 +03:00
Descriptor-anchored Secured Filesystem Operations
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user