mirror of
https://github.com/telemt/telemt.git
synced 2026-10-28 11:37:14 +03:00
bc1c130f38
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
179 lines
7.0 KiB
Rust
179 lines
7.0 KiB
Rust
use super::*;
|
|
|
|
pub(super) fn validate_vhosts(config: &mut ProxyConfig) -> Result<()> {
|
|
let limits = &config.web.limits;
|
|
if config.web.vhosts.len() > limits.max_vhosts {
|
|
return config_error("web.vhosts exceeds web.limits.max_vhosts");
|
|
}
|
|
let mut hosts = HashSet::with_capacity(config.web.vhosts.len());
|
|
let mut profile_count = 0usize;
|
|
for (vhost_idx, vhost) in config.web.vhosts.iter_mut().enumerate() {
|
|
vhost.host = normalize_web_host(&vhost.host, &format!("web.vhosts[{vhost_idx}].host"))?;
|
|
validate_web_base_path(
|
|
&vhost.base_path,
|
|
&format!("web.vhosts[{vhost_idx}].base_path"),
|
|
)?;
|
|
if !hosts.insert(vhost.host.clone()) {
|
|
return config_error(&format!("duplicate WEB vhost host `{}`", vhost.host));
|
|
}
|
|
if vhost.public_addr.port() != 443 || vhost.public_addr.ip().is_unspecified() {
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].public_addr must be a concrete socket address on port 443"
|
|
));
|
|
}
|
|
if config.web.enabled && vhost.profiles.is_empty() {
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].profiles must be non-empty when web.enabled=true"
|
|
));
|
|
}
|
|
validate_decoy(vhost_idx, &vhost.decoy)?;
|
|
let mut profiles = HashSet::with_capacity(vhost.profiles.len());
|
|
for (profile_idx, profile) in vhost.profiles.iter().enumerate() {
|
|
if profile.user.is_empty() || profile.user.len() > 64 {
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].profiles[{profile_idx}].user must contain 1..64 bytes"
|
|
));
|
|
}
|
|
if !config.access.users.contains_key(&profile.user) {
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].profiles[{profile_idx}].user references unknown access user `{}`",
|
|
profile.user
|
|
));
|
|
}
|
|
if !profiles.insert((profile.user.as_str(), profile.secret_mode)) {
|
|
return config_error(&format!(
|
|
"duplicate WEB profile for user `{}` in vhost `{}`",
|
|
profile.user, vhost.host
|
|
));
|
|
}
|
|
let max_streams = profile.max_streams.unwrap_or(limits.max_streams_global);
|
|
let max_streams_per_session = profile
|
|
.max_streams_per_session
|
|
.unwrap_or(limits.max_streams_per_session);
|
|
if profile.max_sessions == Some(0)
|
|
|| profile
|
|
.max_sessions
|
|
.is_some_and(|value| value > limits.max_sessions_global)
|
|
|| profile.max_streams == Some(0)
|
|
|| profile
|
|
.max_streams
|
|
.is_some_and(|value| value > limits.max_streams_global)
|
|
|| profile.max_streams_per_session == Some(0)
|
|
|| profile
|
|
.max_streams_per_session
|
|
.is_some_and(|value| value > limits.max_streams_per_session)
|
|
|| max_streams_per_session > max_streams
|
|
{
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].profiles[{profile_idx}] limits must be non-zero and within global WEB limits"
|
|
));
|
|
}
|
|
profile_count = profile_count.checked_add(1).ok_or_else(|| {
|
|
ProxyError::Config("WEB profile count overflowed usize".to_string())
|
|
})?;
|
|
}
|
|
}
|
|
if profile_count > limits.max_profiles {
|
|
return config_error("WEB profiles exceed web.limits.max_profiles");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_web_base_path(value: &str, field: &str) -> Result<()> {
|
|
let valid = value.len() <= 128
|
|
&& !value.starts_with('/')
|
|
&& !value.ends_with('/')
|
|
&& value.split('/').all(|segment| {
|
|
let mut bytes = segment.bytes();
|
|
bytes
|
|
.next()
|
|
.is_some_and(|byte| byte.is_ascii_alphanumeric())
|
|
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
|
|
});
|
|
if value.is_empty() || valid {
|
|
return Ok(());
|
|
}
|
|
config_error(&format!(
|
|
"{field} must be empty or contain at most 128 ASCII bytes in slash-separated [A-Za-z0-9][A-Za-z0-9_-]* segments"
|
|
))
|
|
}
|
|
|
|
pub(super) fn normalize_web_host(value: &str, field: &str) -> Result<String> {
|
|
let input = value.trim();
|
|
if input.is_empty()
|
|
|| input.ends_with('.')
|
|
|| input
|
|
.chars()
|
|
.any(|character| matches!(character, ':' | '/' | '?' | '#' | '@'))
|
|
{
|
|
return config_error(&format!(
|
|
"{field} must be a hostname without a port, path, credentials, or trailing dot"
|
|
));
|
|
}
|
|
let host = normalize_domain_to_ascii(input, field)?;
|
|
if host.len() > 253
|
|
|| !host.contains('.')
|
|
|| host.parse::<IpAddr>().is_ok()
|
|
|| web_host_last_label_is_numeric(&host)
|
|
{
|
|
return config_error(&format!(
|
|
"{field} must be a non-IP fully-qualified hostname accepted by Telegram Desktop"
|
|
));
|
|
}
|
|
for label in host.split('.') {
|
|
if label.is_empty()
|
|
|| label.len() > 63
|
|
|| label.starts_with('-')
|
|
|| label.ends_with('-')
|
|
|| !label
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
|
{
|
|
return config_error(&format!(
|
|
"{field} contains a hostname label rejected by Telegram Desktop"
|
|
));
|
|
}
|
|
}
|
|
Ok(host)
|
|
}
|
|
|
|
pub(super) fn web_host_last_label_is_numeric(host: &str) -> bool {
|
|
let label = host.rsplit('.').next().unwrap_or_default();
|
|
let digits = label
|
|
.strip_prefix("0x")
|
|
.or_else(|| label.strip_prefix("0X"));
|
|
if let Some(digits) = digits {
|
|
return digits.bytes().all(|byte| byte.is_ascii_hexdigit());
|
|
}
|
|
label.bytes().all(|byte| byte.is_ascii_digit())
|
|
}
|
|
|
|
pub(super) fn validate_decoy(vhost_idx: usize, decoy: &WebDecoyConfig) -> Result<()> {
|
|
match decoy {
|
|
WebDecoyConfig::HttpUpstream { upstream, resolve } => {
|
|
decoy_dns::parse_origin(vhost_idx, upstream, *resolve)?;
|
|
}
|
|
WebDecoyConfig::StaticDirectory { directory, index } => {
|
|
if !directory.is_absolute() {
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].decoy.directory must be absolute"
|
|
));
|
|
}
|
|
if index.is_empty()
|
|
|| index.contains('\\')
|
|
|| std::path::Path::new(index).components().count() != 1
|
|
|| matches!(index.as_str(), "." | "..")
|
|
{
|
|
return config_error(&format!(
|
|
"web.vhosts[{vhost_idx}].decoy.index must be one safe file name"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn config_error<T>(message: &str) -> Result<T> {
|
|
Err(ProxyError::Config(message.to_string()))
|
|
}
|