Skip to main content

alun_utils/
web.rs

1//! Web 解析工具:URL 解析、IP 提取、User-Agent 解析等
2
3use std::net::SocketAddr;
4use url::Url;
5
6/// 检查是否为私有 IP(IPv4/IPv6)
7///
8/// IPv4 私有地址包括:私有地址(10/8, 172.16/12, 192.168/16)、环回地址(127/8)、
9/// 链路本地地址(169.254/16)、未指定地址(0.0.0.0)、组播地址(224/4)
10///
11/// IPv6 私有地址包括:环回地址(::1)、未指定地址(::)、链路本地单播(fe80::/10)、
12/// 唯一本地地址(fc00::/7)、组播地址(ff::/8)
13///
14/// # 参数
15/// - `ip`: IP 地址字符串
16///
17/// # 返回
18/// 若为私有 IP 返回 true,否则返回 false
19pub fn is_private_ip(ip: &str) -> bool {
20    if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
21        match addr {
22            std::net::IpAddr::V4(ipv4) => {
23                ipv4.is_private()
24                    || ipv4.is_loopback()
25                    || ipv4.is_link_local()
26                    || ipv4.is_unspecified()
27                    || ipv4.is_multicast()
28            }
29            std::net::IpAddr::V6(ipv6) => {
30                ipv6.is_loopback()
31                    || ipv6.is_unspecified()
32                    || ipv6.is_unicast_link_local()
33                    || ipv6.is_unique_local()
34                    || ipv6.is_multicast()
35            }
36        }
37    } else {
38        false
39    }
40}
41
42/// 从请求头提取客户端真实 IP
43///
44/// # 优先级
45/// 1. CF-Connecting-IP(Cloudflare CDN)
46/// 2. X-Forwarded-For(首个非私有 IP,AWS ELB/代理场景)
47/// 3. X-Real-IP / X-Client-IP / X-Cluster-Client-IP
48/// 4. 连接地址 IP
49/// 5. 回退至 "0.0.0.0"
50///
51/// # 参数
52/// - `headers`: HTTP 请求头
53/// - `connect_info`: 连接地址信息
54///
55/// # 返回
56/// 客户端真实 IP 字符串,若无法获取则返回 "0.0.0.0"
57pub fn extract_client_ip(headers: &http::HeaderMap, connect_info: &SocketAddr) -> String {
58    if let Some(cf_ip) = headers.get("CF-Connecting-IP").and_then(|h| h.to_str().ok()) {
59        return cf_ip.to_string();
60    }
61
62    if let Some(x_forwarded_for) = headers.get("X-Forwarded-For").and_then(|h| h.to_str().ok()) {
63        for ip in x_forwarded_for.split(',') {
64            let trimmed = ip.trim();
65            if !is_private_ip(trimmed) {
66                return trimmed.to_string();
67            }
68        }
69    }
70
71    let other_headers = ["X-Real-IP", "X-Client-IP", "X-Cluster-Client-IP"];
72    for header_name in &other_headers {
73        if let Some(ip) = headers.get(*header_name).and_then(|h| h.to_str().ok()) {
74            if !is_private_ip(ip) {
75                return ip.to_string();
76            }
77        }
78    }
79
80    let connect_ip = connect_info.ip().to_string();
81    if !is_private_ip(&connect_ip) {
82        return connect_ip;
83    }
84
85    "0.0.0.0".to_string()
86}
87
88/// Web 解析工具
89///
90/// 提供 URL 解析、真实 IP 获取、私网 IP 判断、查询字符串构造等功能。
91pub struct WebExt;
92
93impl WebExt {
94    /// 解析 URL 获取域名
95    pub fn domain(url_str: &str) -> Option<String> {
96        Url::parse(url_str)
97            .ok()
98            .and_then(|u| u.host_str().map(|s| s.to_string()))
99    }
100
101    /// 解析 URL 获取路径
102    pub fn path(url_str: &str) -> Option<String> {
103        Url::parse(url_str).ok().map(|u| u.path().to_string())
104    }
105
106    /// 从请求头获取真实 IP(X-Forwarded-For 或 X-Real-IP)
107    pub fn real_ip(headers: &[(String, String)], remote_addr: &str) -> String {
108        for (key, val) in headers {
109            if key.to_lowercase() == "x-forwarded-for" {
110                return val.split(',').next().unwrap_or("").trim().to_string();
111            }
112            if key.to_lowercase() == "x-real-ip" {
113                return val.clone();
114            }
115        }
116        remote_addr
117            .split(':')
118            .next()
119            .unwrap_or(remote_addr)
120            .to_string()
121    }
122    /// 检查是否为私有 IP(委托给公共函数 `is_private_ip`)
123    pub fn is_private_ip(ip: &str) -> bool {
124        is_private_ip(ip)
125    }
126    /// 构建 URL 查询字符串
127    pub fn build_query(params: &[(&str, &str)]) -> String {
128        if params.is_empty() {
129            return String::new();
130        }
131        let parts: Vec<String> = params
132            .iter()
133            .map(|(k, v)| format!("{}={}", urlencoding(k), urlencoding(v)))
134            .collect();
135        format!("?{}", parts.join("&"))
136    }
137}
138
139fn urlencoding(s: &str) -> String {
140    let mut result = String::with_capacity(s.len() * 3);
141    for byte in s.bytes() {
142        match byte {
143            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
144                result.push(byte as char);
145            }
146            _ => {
147                result.push_str(&format!("%{:02X}", byte));
148            }
149        }
150    }
151    result
152}