use std::{
borrow::Cow,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use rocket::http::{HeaderMap, uncased::Uncased};
use crate::canonical::canonical_ip;
#[derive(Clone, Copy, Debug)]
pub(crate) enum ChainEntry {
Ip(IpAddr),
Opaque,
}
#[inline]
pub(crate) fn header_lines<'a>(headers: &'a HeaderMap<'_>, name: &Uncased<'_>) -> Vec<&'a str> {
headers.get(name.as_str()).collect()
}
pub(crate) fn configured_client_ip_header(
headers: &HeaderMap<'_>,
header: &Uncased<'_>,
) -> Option<IpAddr> {
let raw = headers.get(header.as_str()).last()?;
if !is_visible_ascii(raw) {
return None;
}
Some(canonical_ip(raw.trim().parse::<IpAddr>().ok()?))
}
pub(crate) fn list_header_entries(
lines: Vec<&str>,
) -> Option<impl DoubleEndedIterator<Item = ChainEntry> + '_> {
if lines.iter().any(|line| !is_visible_ascii(line)) {
return None;
}
Some(
lines
.into_iter()
.flat_map(|raw| raw.split(','))
.map(str::trim)
.filter(|part| !part.is_empty())
.map(chain_entry),
)
}
pub(crate) fn forwarded_entries(lines: Vec<&str>) -> Option<Vec<ChainEntry>> {
let mut entries = Vec::new();
for raw in lines {
if !is_visible_ascii(raw) {
return None;
}
for element in SplitQuoted::new(raw, ',') {
if element.is_empty() {
continue;
}
entries.push(forwarded_element_entry(element));
}
}
Some(entries)
}
fn forwarded_element_entry(element: &str) -> ChainEntry {
for pair in SplitQuoted::new(element, ';') {
let Some((name, value)) = pair.split_once('=') else {
continue;
};
if !name.trim().eq_ignore_ascii_case("for") {
continue;
}
return chain_entry(&unquote_http_quoted_string(value.trim()));
}
ChainEntry::Opaque
}
#[inline]
fn chain_entry(raw: &str) -> ChainEntry {
match parse_ip_like(raw) {
Some(ip) => ChainEntry::Ip(canonical_ip(ip)),
None => ChainEntry::Opaque,
}
}
#[inline]
fn is_visible_ascii(line: &str) -> bool {
line.bytes().all(|byte| matches!(byte, 0x20..=0x7E | b'\t'))
}
struct SplitQuoted<'a> {
input: &'a str,
delimiter: char,
start: Option<usize>,
}
impl<'a> SplitQuoted<'a> {
#[inline]
const fn new(input: &'a str, delimiter: char) -> Self {
Self {
input,
delimiter,
start: Some(0),
}
}
}
impl<'a> Iterator for SplitQuoted<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let start = self.start?;
let mut in_quotes = false;
let mut escaped = false;
for (offset, ch) in self.input[start..].char_indices() {
if escaped {
escaped = false;
continue;
}
if in_quotes && ch == '\\' {
escaped = true;
continue;
}
if ch == '"' {
in_quotes = !in_quotes;
continue;
}
if ch == self.delimiter && !in_quotes {
let end = start + offset;
self.start = Some(end + ch.len_utf8());
return Some(self.input[start..end].trim());
}
}
self.start = None;
Some(self.input[start..].trim())
}
}
#[inline]
fn unquote_http_quoted_string(input: &str) -> Cow<'_, str> {
let input = input.trim();
if input.len() < 2 || !input.starts_with('"') || !input.ends_with('"') {
return Cow::Borrowed(input);
}
let inner = &input[1..input.len() - 1];
if !inner.contains('\\') {
return Cow::Borrowed(inner);
}
let mut output = String::with_capacity(inner.len());
let mut escaped = false;
for ch in inner.chars() {
if escaped {
output.push(ch);
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
output.push(ch);
}
Cow::Owned(output)
}
#[inline]
fn parse_ip_like(raw: &str) -> Option<IpAddr> {
let raw = raw.trim();
if raw.is_empty() || raw.eq_ignore_ascii_case("unknown") || raw.starts_with('_') {
return None;
}
if let Ok(ip) = raw.parse::<IpAddr>() {
return Some(ip);
}
if let Some(rest) = raw.strip_prefix('[') {
let close_bracket = rest.find(']')?;
let ip_part = &rest[..close_bracket];
let tail = &rest[close_bracket + 1..];
if tail.is_empty() {
return ip_part.parse::<IpAddr>().ok();
}
if let Some(port) = tail.strip_prefix(':')
&& is_valid_node_port(port)
{
return ip_part.parse::<IpAddr>().ok();
}
return None;
}
if let Ok(socket_addr) = raw.parse::<SocketAddr>() {
return Some(socket_addr.ip());
}
if let Some((host, port)) = raw.rsplit_once(':')
&& host.parse::<Ipv4Addr>().is_ok()
&& is_valid_node_port(port)
{
return host.parse::<IpAddr>().ok();
}
None
}
#[inline]
fn is_valid_node_port(port: &str) -> bool {
if port.is_empty() {
return false;
}
if port.chars().all(|ch| ch.is_ascii_digit()) {
return true;
}
let Some(obfuscated) = port.strip_prefix('_') else {
return false;
};
!obfuscated.is_empty()
&& obfuscated.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-'))
}