Fix support for Chinese domain names (#1462)
This commit is contained in:
Generated
+1
@@ -2152,6 +2152,7 @@ dependencies = [
|
|||||||
"http_req",
|
"http_req",
|
||||||
"humansize",
|
"humansize",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
|
"idna 1.0.3",
|
||||||
"kcp-sys",
|
"kcp-sys",
|
||||||
"machine-uid",
|
"machine-uid",
|
||||||
"maplit",
|
"maplit",
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ anyhow = "1.0"
|
|||||||
|
|
||||||
url = { version = "2.5", features = ["serde"] }
|
url = { version = "2.5", features = ["serde"] }
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
|
idna = "1.0"
|
||||||
|
|
||||||
# for tun packet
|
# for tun packet
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
use idna::domain_to_ascii;
|
||||||
|
pub fn convert_idn_to_ascii(url_str: &str) -> Result<String, String> {
|
||||||
|
if !url_str.is_ascii() {
|
||||||
|
let mut url_parts = url_str.splitn(2, "://");
|
||||||
|
let scheme = url_parts.next().unwrap_or("");
|
||||||
|
let rest = url_parts.next().unwrap_or(url_str);
|
||||||
|
let (host_part, port_part, path_part) = {
|
||||||
|
let mut path_and_rest = rest.splitn(2, '/');
|
||||||
|
let host_port_part = path_and_rest.next().unwrap_or("");
|
||||||
|
let path_part = path_and_rest
|
||||||
|
.next()
|
||||||
|
.map(|s| format!("/{}", s))
|
||||||
|
.unwrap_or_default();
|
||||||
|
if host_port_part.starts_with('[') {
|
||||||
|
if let Some(end_bracket_pos) = host_port_part.find(']') {
|
||||||
|
let host_part = &host_port_part[..end_bracket_pos + 1];
|
||||||
|
let remaining = &host_port_part[end_bracket_pos + 1..];
|
||||||
|
if remaining.starts_with(':') {
|
||||||
|
if let Some(port_str) = remaining.strip_prefix(':') {
|
||||||
|
if port_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
(host_part, format!(":{}", port_str), path_part)
|
||||||
|
} else {
|
||||||
|
(host_part, String::new(), path_part)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(host_part, String::new(), path_part)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(host_part, String::new(), path_part)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(host_port_part, String::new(), path_part)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (host_part, port_part) = if let Some(pos) = host_port_part.rfind(':') {
|
||||||
|
let port_str = &host_port_part[pos + 1..];
|
||||||
|
if port_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
(&host_port_part[..pos], format!(":{}", port_str))
|
||||||
|
} else {
|
||||||
|
(host_port_part, String::new())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(host_port_part, String::new())
|
||||||
|
};
|
||||||
|
(host_part, port_part, path_part)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !host_part.is_ascii() {
|
||||||
|
let ascii_host = domain_to_ascii(host_part)
|
||||||
|
.map_err(|e| format!("Failed to convert IDN to ASCII: {}", e))?;
|
||||||
|
let result = format!("{}://{}{}{}", scheme, ascii_host, port_part, path_part);
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
Ok(url_str.to_string())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(url_str.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn safe_convert_idn_to_ascii(url_str: &str) -> String {
|
||||||
|
convert_idn_to_ascii(url_str).unwrap_or_else(|_| url_str.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ascii_only_urls() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://example.com").unwrap(),
|
||||||
|
"https://example.com"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("http://test.org:8080/path").unwrap(),
|
||||||
|
"http://test.org:8080/path"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_domains() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://räksmörgås.nu").unwrap(),
|
||||||
|
"https://xn--rksmrgs-5wao1o.nu"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://例子.测试").unwrap(),
|
||||||
|
"https://xn--fsqu00a.xn--0zwm56d"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chinese_domains() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://中文.测试").unwrap(),
|
||||||
|
"https://xn--fiq228c.xn--0zwm56d"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://公司.中国").unwrap(),
|
||||||
|
"https://xn--55qx5d.xn--fiqs8s"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://网络.测试").unwrap(),
|
||||||
|
"https://xn--io0a7i.xn--0zwm56d"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_domains_with_port() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://räksmörgås.nu:8080").unwrap(),
|
||||||
|
"https://xn--rksmrgs-5wao1o.nu:8080"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("http://例子.测试:3000/path").unwrap(),
|
||||||
|
"http://xn--fsqu00a.xn--0zwm56d:3000/path"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://中文.测试:9000/api").unwrap(),
|
||||||
|
"https://xn--fiq228c.xn--0zwm56d:9000/api"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_domains_with_path() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://räksmörgås.nu/path/to/resource").unwrap(),
|
||||||
|
"https://xn--rksmrgs-5wao1o.nu/path/to/resource"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("http://例子.测试/api/v1").unwrap(),
|
||||||
|
"http://xn--fsqu00a.xn--0zwm56d/api/v1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://中文.测试/api/users").unwrap(),
|
||||||
|
"https://xn--fiq228c.xn--0zwm56d/api/users"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_domains_with_port_and_path() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://räksmörgås.nu:8080/path/to/resource").unwrap(),
|
||||||
|
"https://xn--rksmrgs-5wao1o.nu:8080/path/to/resource"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("http://例子.测试:9000/api/v1/users").unwrap(),
|
||||||
|
"http://xn--fsqu00a.xn--0zwm56d:9000/api/v1/users"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://中文.测试:8000/用户/管理").unwrap(),
|
||||||
|
"https://xn--fiq228c.xn--0zwm56d:8000/用户/管理"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv6_literals() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://[2001:db8::1]:8080").unwrap(),
|
||||||
|
"https://[2001:db8::1]:8080"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://[2001:db8::1]/path").unwrap(),
|
||||||
|
"https://[2001:db8::1]/path"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://[2001:db8::1]/路径/资源").unwrap(),
|
||||||
|
"https://[2001:db8::1]/路径/资源"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_port_format() {
|
||||||
|
let result = convert_idn_to_ascii("https://räksmörgås.nu:notaport").unwrap();
|
||||||
|
assert!(result.contains("xn--") && result.contains(":notaport"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_safe_conversion() {
|
||||||
|
assert_eq!(
|
||||||
|
safe_convert_idn_to_ascii("https://example.com"),
|
||||||
|
"https://example.com"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
safe_convert_idn_to_ascii("https://中文.测试"),
|
||||||
|
"https://xn--fiq228c.xn--0zwm56d"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edge_cases() {
|
||||||
|
// Without scheme '://', entire string is treated as host part
|
||||||
|
let result = convert_idn_to_ascii("räksmörgås.nu").unwrap();
|
||||||
|
assert_eq!(result, "räksmörgås.nu://xn--rksmrgs-5wao1o.nu");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://test.例子.com").unwrap(),
|
||||||
|
"https://test.xn--fsqu00a.com"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ipv6_with_unicode_path() {
|
||||||
|
assert_eq!(
|
||||||
|
convert_idn_to_ascii("https://[2001:db8::1]/路径/资源").unwrap(),
|
||||||
|
"https://[2001:db8::1]/路径/资源"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ pub mod defer;
|
|||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod global_ctx;
|
pub mod global_ctx;
|
||||||
|
pub mod idn;
|
||||||
pub mod ifcfg;
|
pub mod ifcfg;
|
||||||
pub mod netns;
|
pub mod netns;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
|||||||
+21
-12
@@ -10,6 +10,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
constants::EASYTIER_VERSION,
|
constants::EASYTIER_VERSION,
|
||||||
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
|
global_ctx::{EventBusSubscriber, GlobalCtxEvent},
|
||||||
|
idn::safe_convert_idn_to_ascii,
|
||||||
},
|
},
|
||||||
instance::instance::Instance,
|
instance::instance::Instance,
|
||||||
proto::api::instance::list_peer_route_pair,
|
proto::api::instance::list_peer_route_pair,
|
||||||
@@ -523,9 +524,13 @@ impl NetworkConfig {
|
|||||||
{
|
{
|
||||||
NetworkingMethod::PublicServer => {
|
NetworkingMethod::PublicServer => {
|
||||||
let public_server_url = self.public_server_url.clone().unwrap_or_default();
|
let public_server_url = self.public_server_url.clone().unwrap_or_default();
|
||||||
|
let converted_public_server_url = safe_convert_idn_to_ascii(&public_server_url);
|
||||||
cfg.set_peers(vec![PeerConfig {
|
cfg.set_peers(vec![PeerConfig {
|
||||||
uri: public_server_url.parse().with_context(|| {
|
uri: converted_public_server_url.parse().with_context(|| {
|
||||||
format!("failed to parse public server uri: {}", public_server_url)
|
format!(
|
||||||
|
"failed to parse public server uri: {}",
|
||||||
|
converted_public_server_url
|
||||||
|
)
|
||||||
})?,
|
})?,
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
@@ -535,10 +540,11 @@ impl NetworkConfig {
|
|||||||
if peer_url.is_empty() {
|
if peer_url.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let converted_peer_url = safe_convert_idn_to_ascii(peer_url);
|
||||||
peers.push(PeerConfig {
|
peers.push(PeerConfig {
|
||||||
uri: peer_url
|
uri: converted_peer_url.parse().with_context(|| {
|
||||||
.parse()
|
format!("failed to parse peer uri: {}", converted_peer_url)
|
||||||
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
})?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,11 +558,10 @@ impl NetworkConfig {
|
|||||||
if listener_url.is_empty() {
|
if listener_url.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
listener_urls.push(
|
let converted_listener_url = safe_convert_idn_to_ascii(listener_url);
|
||||||
listener_url
|
listener_urls.push(converted_listener_url.parse().with_context(|| {
|
||||||
.parse()
|
format!("failed to parse listener uri: {}", converted_listener_url)
|
||||||
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
})?);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
cfg.set_listeners(listener_urls);
|
cfg.set_listeners(listener_urls);
|
||||||
|
|
||||||
@@ -650,8 +655,12 @@ impl NetworkConfig {
|
|||||||
self.mapped_listeners
|
self.mapped_listeners
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
s.parse()
|
let converted_s = safe_convert_idn_to_ascii(s);
|
||||||
.with_context(|| format!("mapped listener is not a valid url: {}", s))
|
converted_s
|
||||||
|
.parse()
|
||||||
|
.with_context(|| {
|
||||||
|
format!("mapped listener is not a valid url: {}", converted_s)
|
||||||
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
.map(|s: url::Url| {
|
.map(|s: url::Url| {
|
||||||
|
|||||||
Reference in New Issue
Block a user