use std::net::SocketAddr;
use url::Url;
pub fn is_private_ip(ip: &str) -> bool {
if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
match addr {
std::net::IpAddr::V4(ipv4) => {
ipv4.is_private()
|| ipv4.is_loopback()
|| ipv4.is_link_local()
|| ipv4.is_unspecified()
|| ipv4.is_multicast()
}
std::net::IpAddr::V6(ipv6) => {
ipv6.is_loopback()
|| ipv6.is_unspecified()
|| ipv6.is_unicast_link_local()
|| ipv6.is_unique_local()
|| ipv6.is_multicast()
}
}
} else {
false
}
}
pub fn extract_client_ip(headers: &http::HeaderMap, connect_info: &SocketAddr) -> String {
fn is_valid_ip(s: &str) -> bool {
s.trim().parse::<std::net::IpAddr>().is_ok()
}
if let Some(ip) = headers
.get("X-Real-IP")
.and_then(|h| h.to_str().ok())
.filter(|s| is_valid_ip(s))
{
return ip.trim().to_string();
}
if let Some(ip) = headers
.get("X-Forwarded-For")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.split(',').next_back())
.filter(|s| is_valid_ip(s))
{
return ip.trim().to_string();
}
let connect_ip = connect_info.ip().to_string();
if !is_private_ip(&connect_ip) {
return connect_ip;
}
"0.0.0.0".to_string()
}
pub struct WebExt;
impl WebExt {
pub fn domain(url_str: &str) -> Option<String> {
Url::parse(url_str)
.ok()
.and_then(|u| u.host_str().map(|s| s.to_string()))
}
pub fn path(url_str: &str) -> Option<String> {
Url::parse(url_str).ok().map(|u| u.path().to_string())
}
pub fn real_ip(headers: &[(String, String)], remote_addr: &str) -> String {
for (key, val) in headers {
if key.to_lowercase() == "x-forwarded-for" {
return val.split(',').next().unwrap_or("").trim().to_string();
}
if key.to_lowercase() == "x-real-ip" {
return val.clone();
}
}
remote_addr
.split(':')
.next()
.unwrap_or(remote_addr)
.to_string()
}
pub fn is_private_ip(ip: &str) -> bool {
is_private_ip(ip)
}
pub fn build_query(params: &[(&str, &str)]) -> String {
if params.is_empty() {
return String::new();
}
let parts: Vec<String> = params
.iter()
.map(|(k, v)| format!("{}={}", urlencoding(k), urlencoding(v)))
.collect();
format!("?{}", parts.join("&"))
}
}
fn urlencoding(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 3);
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
result.push(byte as char);
}
_ => {
result.push_str(&format!("%{:02X}", byte));
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use http::{HeaderMap, HeaderName, HeaderValue};
use std::net::SocketAddr;
fn header_maps(entries: &[(&str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for (k, v) in entries {
let name: HeaderName = k.parse().unwrap();
let value: HeaderValue = v.parse().unwrap();
map.insert(name, value);
}
map
}
fn connect(ip: &str) -> SocketAddr {
format!("{}:54321", ip).parse().unwrap()
}
#[test]
fn x_real_ip_takes_priority_over_xff_first_segment() {
let headers = header_maps(&[
("x-forwarded-for", "1.2.3.4, 203.0.113.9"),
("x-real-ip", "203.0.113.9"),
]);
assert_eq!(
extract_client_ip(&headers, &connect("10.0.0.5")),
"203.0.113.9"
);
}
#[test]
fn xff_uses_last_segment_not_attacker_first_segment() {
let headers = header_maps(&[("x-forwarded-for", "1.2.3.4, 198.51.100.7")]);
assert_eq!(
extract_client_ip(&headers, &connect("10.0.0.5")),
"198.51.100.7"
);
}
#[test]
fn single_hop_xff_returns_itself() {
let headers = header_maps(&[("x-forwarded-for", "203.0.113.42")]);
assert_eq!(
extract_client_ip(&headers, &connect("10.0.0.5")),
"203.0.113.42"
);
}
#[test]
fn invalid_ip_in_headers_falls_back() {
let headers = header_maps(&[("x-forwarded-for", "not-an-ip, 198.51.100.7")]);
assert_eq!(
extract_client_ip(&headers, &connect("10.0.0.5")),
"198.51.100.7"
);
let bad = header_maps(&[("x-forwarded-for", "evil")]);
assert_eq!(extract_client_ip(&bad, &connect("10.0.0.5")), "0.0.0.0");
}
#[test]
fn private_connect_ip_does_not_leak() {
let headers = header_maps(&[]);
assert_eq!(extract_client_ip(&headers, &connect("10.1.1.1")), "0.0.0.0");
}
}