mirror of
https://github.com/telemt/telemt.git
synced 2026-05-22 19:51:43 +03:00
IDN Support
Signed-off-by: Alexey <247128645+axkurcom@users.noreply.github.com>
This commit is contained in:
@@ -31,6 +31,49 @@ fn is_valid_tls_domain_name(domain: &str) -> bool {
|
|||||||
.any(|ch| ch.is_whitespace() || matches!(ch, '/' | '\\'))
|
.any(|ch| ch.is_whitespace() || matches!(ch, '/' | '\\'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_domain_to_ascii(domain: &str, field: &str) -> Result<String> {
|
||||||
|
let domain = domain.trim();
|
||||||
|
if !is_valid_tls_domain_name(domain) {
|
||||||
|
return Err(ProxyError::Config(format!(
|
||||||
|
"Invalid {field}: '{}'. Must be a valid domain name",
|
||||||
|
domain
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = url::Url::parse(&format!("https://{domain}/")).map_err(|error| {
|
||||||
|
ProxyError::Config(format!(
|
||||||
|
"Invalid {field}: '{}'. IDNA conversion failed: {error}",
|
||||||
|
domain
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let host = parsed.host_str().ok_or_else(|| {
|
||||||
|
ProxyError::Config(format!("Invalid {field}: '{}'. Host is empty", domain))
|
||||||
|
})?;
|
||||||
|
Ok(host.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_mask_host_to_ascii(host: &str, field: &str) -> Result<String> {
|
||||||
|
let host = host.trim();
|
||||||
|
if host.starts_with('[') && host.ends_with(']') {
|
||||||
|
let inner = &host[1..host.len() - 1];
|
||||||
|
let ip = inner.parse::<std::net::IpAddr>().map_err(|_| {
|
||||||
|
ProxyError::Config(format!("Invalid {field}: '{}'. IPv6 literal is invalid", host))
|
||||||
|
})?;
|
||||||
|
return match ip {
|
||||||
|
std::net::IpAddr::V6(v6) => Ok(format!("[{v6}]")),
|
||||||
|
std::net::IpAddr::V4(v4) => Ok(v4.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||||
|
return match ip {
|
||||||
|
std::net::IpAddr::V4(v4) => Ok(v4.to_string()),
|
||||||
|
std::net::IpAddr::V6(v6) => Ok(format!("[{v6}]")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_domain_to_ascii(host, field)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_exclusive_mask_target(target: &str) -> Option<(&str, u16)> {
|
fn parse_exclusive_mask_target(target: &str) -> Option<(&str, u16)> {
|
||||||
let target = target.trim();
|
let target = target.trim();
|
||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
@@ -55,6 +98,17 @@ fn parse_exclusive_mask_target(target: &str) -> Option<(&str, u16)> {
|
|||||||
(port > 0).then_some((host, port))
|
(port > 0).then_some((host, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_exclusive_mask_target(target: &str, field: &str) -> Result<String> {
|
||||||
|
let (host, port) = parse_exclusive_mask_target(target).ok_or_else(|| {
|
||||||
|
ProxyError::Config(format!(
|
||||||
|
"Invalid {field}: '{}'. Expected host:port with port > 0",
|
||||||
|
target
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let host = normalize_mask_host_to_ascii(host, field)?;
|
||||||
|
Ok(format!("{host}:{port}"))
|
||||||
|
}
|
||||||
|
|
||||||
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
|
||||||
"general",
|
"general",
|
||||||
"network",
|
"network",
|
||||||
@@ -1912,10 +1966,8 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tls_domain.
|
config.censorship.tls_domain =
|
||||||
if config.censorship.tls_domain.is_empty() {
|
normalize_domain_to_ascii(&config.censorship.tls_domain, "censorship.tls_domain")?;
|
||||||
return Err(ProxyError::Config("tls_domain cannot be empty".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mask_unix_sock.
|
// Validate mask_unix_sock.
|
||||||
if let Some(ref sock_path) = config.censorship.mask_unix_sock {
|
if let Some(ref sock_path) = config.censorship.mask_unix_sock {
|
||||||
@@ -1943,6 +1995,10 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(mask_host) = config.censorship.mask_host.as_mut() {
|
||||||
|
*mask_host = normalize_mask_host_to_ascii(mask_host, "censorship.mask_host")?;
|
||||||
|
}
|
||||||
|
|
||||||
// Default mask_host to tls_domain if not set and no unix socket configured.
|
// Default mask_host to tls_domain if not set and no unix socket configured.
|
||||||
if config.censorship.mask_host.is_none() && config.censorship.mask_unix_sock.is_none() {
|
if config.censorship.mask_host.is_none() && config.censorship.mask_unix_sock.is_none() {
|
||||||
config.censorship.mask_host = Some(config.censorship.tls_domain.clone());
|
config.censorship.mask_host = Some(config.censorship.tls_domain.clone());
|
||||||
@@ -1993,8 +2049,11 @@ impl ProxyConfig {
|
|||||||
let mut all = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
let mut all = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
||||||
all.push(config.censorship.tls_domain.clone());
|
all.push(config.censorship.tls_domain.clone());
|
||||||
for d in std::mem::take(&mut config.censorship.tls_domains) {
|
for d in std::mem::take(&mut config.censorship.tls_domains) {
|
||||||
if !d.is_empty() && !all.contains(&d) {
|
if !d.is_empty() {
|
||||||
all.push(d);
|
let domain = normalize_domain_to_ascii(&d, "censorship.tls_domains entry")?;
|
||||||
|
if !all.contains(&domain) {
|
||||||
|
all.push(domain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// keep primary as tls_domain; store remaining back to tls_domains
|
// keep primary as tls_domain; store remaining back to tls_domains
|
||||||
@@ -2003,6 +2062,20 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut exclusive_mask = HashMap::with_capacity(config.censorship.exclusive_mask.len());
|
||||||
|
for (domain, target) in std::mem::take(&mut config.censorship.exclusive_mask) {
|
||||||
|
let domain = normalize_domain_to_ascii(
|
||||||
|
&domain,
|
||||||
|
"censorship.exclusive_mask domain",
|
||||||
|
)?;
|
||||||
|
let target = normalize_exclusive_mask_target(
|
||||||
|
&target,
|
||||||
|
"censorship.exclusive_mask target",
|
||||||
|
)?;
|
||||||
|
exclusive_mask.insert(domain, target);
|
||||||
|
}
|
||||||
|
config.censorship.exclusive_mask = exclusive_mask;
|
||||||
|
|
||||||
// Migration: prefer_ipv6 -> network.prefer.
|
// Migration: prefer_ipv6 -> network.prefer.
|
||||||
if config.general.prefer_ipv6 {
|
if config.general.prefer_ipv6 {
|
||||||
if config.network.prefer == 4 {
|
if config.network.prefer == 4 {
|
||||||
@@ -2731,20 +2804,28 @@ mod tests {
|
|||||||
[server]
|
[server]
|
||||||
[access]
|
[access]
|
||||||
[censorship]
|
[censorship]
|
||||||
tls_domain = "example.com"
|
tls_domain = "weißbiergärten.de"
|
||||||
|
tls_domains = ["bürgeramt.de"]
|
||||||
[censorship.exclusive_mask]
|
[censorship.exclusive_mask]
|
||||||
"my-site.com" = "127.0.0.1:8443"
|
"bürgeramt.de" = "rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz.de:443"
|
||||||
"ipv6.example" = "[::1]:9443"
|
"ipv6.example" = "[::1]:443"
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(cfg.censorship.tls_domain, "xn--weibiergrten-n9a9e.de");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.censorship.exclusive_mask.get("my-site.com"),
|
cfg.censorship.tls_domains,
|
||||||
Some(&"127.0.0.1:8443".to_string())
|
vec!["xn--brgeramt-n4a.de".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.censorship
|
||||||
|
.exclusive_mask
|
||||||
|
.get("xn--brgeramt-n4a.de"),
|
||||||
|
Some(&"xn--rindfleischetikettierungsberwachungsaufgabenbertragungsgesetz-nkgt.de:443".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.censorship.exclusive_mask.get("ipv6.example"),
|
cfg.censorship.exclusive_mask.get("ipv6.example"),
|
||||||
Some(&"[::1]:9443".to_string())
|
Some(&"[::1]:443".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user