use super::traits::{Identifier, IdentifierExtractor, RequestContext};
use crate::config::TrustedProxyConfig;
use std::net::IpAddr;
pub struct UserIdExtractor {
header_name: Option<String>,
query_param_name: Option<String>,
default_user_id: Option<String>,
}
impl UserIdExtractor {
pub fn new(
header_name: Option<String>,
query_param_name: Option<String>,
default_user_id: Option<String>,
) -> Self {
Self {
header_name,
query_param_name,
default_user_id,
}
}
pub fn from_header(header_name: &str) -> Self {
Self::new(Some(header_name.to_string()), None, None)
}
pub fn from_query_param(query_param_name: &str) -> Self {
Self::new(None, Some(query_param_name.to_string()), None)
}
pub fn with_default(mut self, default_user_id: &str) -> Self {
self.default_user_id = Some(default_user_id.to_string());
self
}
pub fn builder() -> UserIdExtractorBuilder {
UserIdExtractorBuilder::new()
}
pub fn with_dependencies(
header_name: Option<String>,
query_param_name: Option<String>,
default_user_id: Option<String>,
) -> Self {
Self::new(header_name, query_param_name, default_user_id)
}
}
#[derive(Debug, Clone, Default)]
pub struct UserIdExtractorBuilder {
header_name: Option<String>,
query_param_name: Option<String>,
default_user_id: Option<String>,
}
impl UserIdExtractorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn header_name(mut self, header_name: &str) -> Self {
self.header_name = Some(header_name.to_string());
self
}
pub fn query_param_name(mut self, query_param_name: &str) -> Self {
self.query_param_name = Some(query_param_name.to_string());
self
}
pub fn default_user_id(mut self, default_user_id: &str) -> Self {
self.default_user_id = Some(default_user_id.to_string());
self
}
pub fn build(self) -> UserIdExtractor {
UserIdExtractor::new(
self.header_name,
self.query_param_name,
self.default_user_id,
)
}
}
impl IdentifierExtractor for UserIdExtractor {
fn extract(&self, context: &RequestContext) -> Option<Identifier> {
if let Some(header_name) = &self.header_name {
if let Some(user_id) = context.get_header(header_name) {
if !user_id.is_empty() {
return Some(Identifier::UserId(user_id.clone()));
}
}
}
if let Some(query_param_name) = &self.query_param_name {
if let Some(user_id) = context.query_params.get(query_param_name) {
if !user_id.is_empty() {
return Some(Identifier::UserId(user_id.clone()));
}
}
}
if let Some(default) = &self.default_user_id {
return Some(Identifier::UserId(default.clone()));
}
None
}
fn name(&self) -> &str {
"UserIdExtractor"
}
}
pub struct IpExtractor {
header_names: Vec<String>,
validate: bool,
trusted_proxy_config: TrustedProxyConfig,
}
impl IpExtractor {
pub fn new(header_names: Vec<String>, validate: bool) -> Self {
Self {
header_names,
validate,
trusted_proxy_config: TrustedProxyConfig::default(),
}
}
pub fn with_trusted_proxies(
header_names: Vec<String>,
validate: bool,
trusted_proxy_config: TrustedProxyConfig,
) -> Self {
Self {
header_names,
validate,
trusted_proxy_config,
}
}
pub fn new_default() -> Self {
Self::new(vec![], true)
}
pub fn from_header(header_name: &str) -> Self {
Self::new(vec![header_name.to_string()], true)
}
pub fn from_headers(header_names: Vec<&str>) -> Self {
Self::new(header_names.iter().map(|s| s.to_string()).collect(), true)
}
pub fn builder() -> IpExtractorBuilder {
IpExtractorBuilder::new()
}
pub fn with_dependencies(header_names: Vec<String>, validate: bool) -> Self {
Self::new(header_names, validate)
}
fn parse_ip(&self, value: &str) -> Option<String> {
let ips: Vec<&str> = value
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if ips.is_empty() {
return None;
}
if ips.len() > self.trusted_proxy_config.max_hops {
log::warn!(
"X-Forwarded-For 包含 {} 个 IP,超过最大限制 {}",
ips.len(),
self.trusted_proxy_config.max_hops
);
return None;
}
if ips.len() == 1 {
let ip = ips[0];
if self.validate && ip.parse::<IpAddr>().is_err() {
return None;
}
return Some(ip.to_string());
}
if self.trusted_proxy_config.enabled {
for ip_str in ips.iter().rev() {
if self.validate && ip_str.parse::<IpAddr>().is_err() {
continue;
}
if !self.trusted_proxy_config.is_trusted(ip_str) {
return Some(ip_str.to_string());
}
}
if let Some(&ip) = ips.last() {
if self.validate && ip.parse::<IpAddr>().is_err() {
return None;
}
return Some(ip.to_string());
}
None
} else {
let ip = ips[0];
if self.validate && ip.parse::<IpAddr>().is_err() {
return None;
}
Some(ip.to_string())
}
}
}
#[derive(Debug, Clone, Default)]
pub struct IpExtractorBuilder {
header_names: Vec<String>,
validate: bool,
trusted_proxy_config: Option<TrustedProxyConfig>,
}
impl IpExtractorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn header_name(mut self, header_name: &str) -> Self {
self.header_names.push(header_name.to_string());
self
}
pub fn header_names(mut self, header_names: Vec<&str>) -> Self {
self.header_names = header_names.iter().map(|s| s.to_string()).collect();
self
}
pub fn validate(mut self, validate: bool) -> Self {
self.validate = validate;
self
}
pub fn trusted_proxy_config(mut self, config: TrustedProxyConfig) -> Self {
self.trusted_proxy_config = Some(config);
self
}
pub fn build(self) -> IpExtractor {
match self.trusted_proxy_config {
Some(config) => {
IpExtractor::with_trusted_proxies(self.header_names, self.validate, config)
}
None => IpExtractor::new(self.header_names, self.validate),
}
}
}
impl IdentifierExtractor for IpExtractor {
fn extract(&self, context: &RequestContext) -> Option<Identifier> {
for header_name in &self.header_names {
if let Some(value) = context.get_header(header_name) {
if let Some(ip) = self.parse_ip(value) {
return Some(Identifier::Ip(ip));
}
}
}
if let Some(client_ip) = &context.client_ip {
if let Some(ip) = self.parse_ip(client_ip) {
return Some(Identifier::Ip(ip));
}
}
None
}
fn name(&self) -> &str {
"IpExtractor"
}
}
pub struct MacExtractor {
header_name: Option<String>,
query_param_name: Option<String>,
validate: bool,
}
impl MacExtractor {
pub fn new(
header_name: Option<String>,
query_param_name: Option<String>,
validate: bool,
) -> Self {
Self {
header_name,
query_param_name,
validate,
}
}
pub fn from_header(header_name: &str) -> Self {
Self::new(Some(header_name.to_string()), None, true)
}
pub fn from_query_param(query_param_name: &str) -> Self {
Self::new(None, Some(query_param_name.to_string()), true)
}
pub fn builder() -> MacExtractorBuilder {
MacExtractorBuilder::new()
}
pub fn with_dependencies(
header_name: Option<String>,
query_param_name: Option<String>,
validate: bool,
) -> Self {
Self::new(header_name, query_param_name, validate)
}
fn validate_mac(&self, mac: &str) -> bool {
if !self.validate {
return true;
}
let cleaned = mac.replace([':', '-', '.'], "");
if cleaned.len() != 12 {
return false;
}
cleaned.chars().all(|c| c.is_ascii_hexdigit())
}
}
#[derive(Debug, Clone, Default)]
pub struct MacExtractorBuilder {
header_name: Option<String>,
query_param_name: Option<String>,
validate: bool,
}
impl MacExtractorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn header_name(mut self, header_name: &str) -> Self {
self.header_name = Some(header_name.to_string());
self
}
pub fn query_param_name(mut self, query_param_name: &str) -> Self {
self.query_param_name = Some(query_param_name.to_string());
self
}
pub fn validate(mut self, validate: bool) -> Self {
self.validate = validate;
self
}
pub fn build(self) -> MacExtractor {
MacExtractor::new(self.header_name, self.query_param_name, self.validate)
}
}
impl IdentifierExtractor for MacExtractor {
fn extract(&self, context: &RequestContext) -> Option<Identifier> {
if let Some(header_name) = &self.header_name {
if let Some(mac) = context.get_header(header_name) {
if !mac.is_empty() && self.validate_mac(mac) {
return Some(Identifier::Mac(mac.clone()));
}
}
}
if let Some(query_param_name) = &self.query_param_name {
if let Some(mac) = context.query_params.get(query_param_name) {
if !mac.is_empty() && self.validate_mac(mac) {
return Some(Identifier::Mac(mac.clone()));
}
}
}
None
}
fn name(&self) -> &str {
"MacExtractor"
}
}
pub struct ApiKeyExtractor {
header_name: Option<String>,
_query_param_name: Option<String>,
prefix: Option<String>,
}
impl ApiKeyExtractor {
pub fn new(
header_name: Option<String>,
query_param_name: Option<String>,
prefix: Option<String>,
) -> Self {
if query_param_name.is_some() {
log::warn!("出于安全考虑,通过查询参数提取API Key已被禁用");
}
Self {
header_name,
_query_param_name: query_param_name,
prefix,
}
}
pub fn from_authorization_header() -> Self {
Self::new(
Some("Authorization".to_string()),
None,
Some("Bearer ".to_string()),
)
}
pub fn from_header(header_name: &str) -> Self {
Self::new(Some(header_name.to_string()), None, None)
}
pub fn from_query_param(query_param_name: &str) -> Self {
Self::new(None, Some(query_param_name.to_string()), None)
}
pub fn builder() -> ApiKeyExtractorBuilder {
ApiKeyExtractorBuilder::new()
}
pub fn with_dependencies(
header_name: Option<String>,
query_param_name: Option<String>,
prefix: Option<String>,
) -> Self {
Self::new(header_name, query_param_name, prefix)
}
fn clean_key(&self, value: &str) -> Option<String> {
let key = if let Some(prefix) = &self.prefix {
value.strip_prefix(prefix)
} else {
Some(value)
}?;
let key = key.trim();
if key.is_empty() {
return None;
}
Some(key.to_string())
}
}
#[derive(Debug, Clone, Default)]
pub struct ApiKeyExtractorBuilder {
header_name: Option<String>,
prefix: Option<String>,
}
impl ApiKeyExtractorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn header_name(mut self, header_name: &str) -> Self {
self.header_name = Some(header_name.to_string());
self
}
pub fn prefix(mut self, prefix: &str) -> Self {
self.prefix = Some(prefix.to_string());
self
}
pub fn build(self) -> ApiKeyExtractor {
ApiKeyExtractor::new(self.header_name, None, self.prefix)
}
}
impl IdentifierExtractor for ApiKeyExtractor {
fn extract(&self, context: &RequestContext) -> Option<Identifier> {
if let Some(header_name) = &self.header_name {
if let Some(value) = context.get_header(header_name) {
if let Some(key) = self.clean_key(value) {
return Some(Identifier::ApiKey(key));
}
}
}
None
}
fn name(&self) -> &str {
"ApiKeyExtractor"
}
}
pub struct DeviceIdExtractor {
header_name: Option<String>,
query_param_name: Option<String>,
}
impl DeviceIdExtractor {
pub fn new(header_name: Option<String>, query_param_name: Option<String>) -> Self {
Self {
header_name,
query_param_name,
}
}
pub fn from_header(header_name: &str) -> Self {
Self::new(Some(header_name.to_string()), None)
}
pub fn from_query_param(query_param_name: &str) -> Self {
Self::new(None, Some(query_param_name.to_string()))
}
pub fn builder() -> DeviceIdExtractorBuilder {
DeviceIdExtractorBuilder::new()
}
pub fn with_dependencies(
header_name: Option<String>,
query_param_name: Option<String>,
) -> Self {
Self::new(header_name, query_param_name)
}
}
impl IdentifierExtractor for DeviceIdExtractor {
fn extract(&self, context: &RequestContext) -> Option<Identifier> {
if let Some(header_name) = &self.header_name {
if let Some(device_id) = context.get_header(header_name) {
if !device_id.is_empty() {
return Some(Identifier::DeviceId(device_id.clone()));
}
}
}
if let Some(query_param_name) = &self.query_param_name {
if let Some(device_id) = context.query_params.get(query_param_name) {
if !device_id.is_empty() {
return Some(Identifier::DeviceId(device_id.clone()));
}
}
}
None
}
fn name(&self) -> &str {
"DeviceIdExtractor"
}
}
#[derive(Debug, Clone, Default)]
pub struct DeviceIdExtractorBuilder {
header_name: Option<String>,
query_param_name: Option<String>,
}
impl DeviceIdExtractorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn header_name(mut self, header_name: &str) -> Self {
self.header_name = Some(header_name.to_string());
self
}
pub fn query_param_name(mut self, query_param_name: &str) -> Self {
self.query_param_name = Some(query_param_name.to_string());
self
}
pub fn build(self) -> DeviceIdExtractor {
DeviceIdExtractor::new(self.header_name, self.query_param_name)
}
}
pub struct CustomExtractor<F>
where
F: Fn(&RequestContext) -> Option<Identifier> + Send + Sync,
{
extractor_fn: F,
name: String,
}
impl<F> CustomExtractor<F>
where
F: Fn(&RequestContext) -> Option<Identifier> + Send + Sync,
{
pub fn new(name: &str, extractor_fn: F) -> Self {
Self {
extractor_fn,
name: name.to_string(),
}
}
}
impl<F> IdentifierExtractor for CustomExtractor<F>
where
F: Fn(&RequestContext) -> Option<Identifier> + Send + Sync,
{
fn extract(&self, context: &RequestContext) -> Option<Identifier> {
(self.extractor_fn)(context)
}
fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
use super::*;
use crate::config::TrustedProxyConfig;
use crate::matchers::CompositeExtractor;
#[test]
fn test_user_id_name() {
let extractor = UserIdExtractor::from_header("X-User-Id");
assert_eq!(extractor.name(), "UserIdExtractor");
}
#[test]
fn test_user_id_with_dependencies() {
let extractor = UserIdExtractor::with_dependencies(
Some("X-User-Id".into()),
Some("uid".into()),
Some("default".into()),
);
let ctx = RequestContext::new().with_header("X-User-Id", "dep-user");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("dep-user".into()))
);
}
#[test]
fn test_user_id_from_header() {
let extractor = UserIdExtractor::from_header("X-User-Id");
let ctx = RequestContext::new().with_header("X-User-Id", "user123");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("user123".into()))
);
}
#[test]
fn test_user_id_from_query_param() {
let extractor = UserIdExtractor::from_query_param("user_id");
let ctx = RequestContext::new().with_query_param("user_id", "quser");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("quser".into()))
);
}
#[test]
fn test_user_id_with_default() {
let extractor = UserIdExtractor::from_header("X-User-Id").with_default("guest");
let ctx = RequestContext::new();
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("guest".into()))
);
}
#[test]
fn test_user_id_missing() {
let extractor = UserIdExtractor::from_header("X-User-Id");
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_user_id_header_priority() {
let extractor =
UserIdExtractor::new(Some("X-User-Id".into()), Some("user_id".into()), None);
let ctx = RequestContext::new()
.with_header("X-User-Id", "from_header")
.with_query_param("user_id", "from_query");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("from_header".into()))
);
}
#[test]
fn test_user_id_empty_header() {
let extractor = UserIdExtractor::from_header("X-User-Id");
let ctx = RequestContext::new().with_header("X-User-Id", "");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_user_id_builder() {
let extractor = UserIdExtractor::builder()
.header_name("X-User-Id")
.query_param_name("uid")
.default_user_id("fallback")
.build();
let ctx = RequestContext::new();
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("fallback".into()))
);
}
#[test]
fn test_ip_name() {
let extractor = IpExtractor::new_default();
assert_eq!(extractor.name(), "IpExtractor");
}
#[test]
fn test_ip_with_dependencies() {
let extractor = IpExtractor::with_dependencies(vec!["X-Real-IP".into()], true);
let ctx = RequestContext::new().with_header("X-Real-IP", "10.0.0.5");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.5".into()))
);
}
#[test]
fn test_ip_parse_empty_after_filter() {
let extractor = IpExtractor::from_header("X-Forwarded-For");
let ctx = RequestContext::new().with_header("X-Forwarded-For", " , ");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_parse_max_hops_exceeded() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec![],
max_hops: 2,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["X-Forwarded-For".into()], true, config);
let ctx =
RequestContext::new().with_header("X-Forwarded-For", "10.0.0.1, 10.0.0.2, 10.0.0.3");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_trusted_proxy_invalid_in_chain() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".into(), "10.0.0.2".into()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["X-Forwarded-For".into()], true, config);
let ctx = RequestContext::new()
.with_header("X-Forwarded-For", "203.0.113.5, not-an-ip, 10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("203.0.113.5".into()))
);
}
#[test]
fn test_ip_trusted_proxy_all_trusted() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".into(), "10.0.0.2".into()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["X-Forwarded-For".into()], true, config);
let ctx = RequestContext::new().with_header("X-Forwarded-For", "10.0.0.1, 10.0.0.2");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.2".into()))
);
}
#[test]
fn test_ip_trusted_proxy_all_trusted_invalid_last() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".into()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["X-Forwarded-For".into()], true, config);
let ctx = RequestContext::new().with_header("X-Forwarded-For", "10.0.0.1, not-an-ip");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_trusted_proxy_all_trusted_no_validation() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".into()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["X-Forwarded-For".into()], false, config);
let ctx = RequestContext::new().with_header("X-Forwarded-For", "10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.1".into()))
);
}
#[test]
fn test_ip_no_headers_fallback_to_client_ip_none() {
let extractor = IpExtractor::new_default();
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_new_default() {
let extractor = IpExtractor::new_default();
let ctx = RequestContext::new().with_client_ip("10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.1".into()))
);
}
#[test]
fn test_ip_from_header() {
let extractor = IpExtractor::from_header("X-Forwarded-For");
let ctx = RequestContext::new().with_header("X-Forwarded-For", "203.0.113.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("203.0.113.1".into()))
);
}
#[test]
fn test_ip_from_headers() {
let extractor = IpExtractor::from_headers(vec!["X-Real-IP", "X-Forwarded-For"]);
let ctx = RequestContext::new()
.with_header("X-Forwarded-For", "203.0.113.1")
.with_header("X-Real-IP", "10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.1".into()))
);
}
#[test]
fn test_ip_single() {
let extractor = IpExtractor::from_header("X-Forwarded-For");
let ctx = RequestContext::new().with_header("X-Forwarded-For", "192.168.1.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("192.168.1.1".into()))
);
}
#[test]
fn test_ip_multi_invalid_first_ip_default() {
let extractor = IpExtractor::builder()
.header_name("X-Forwarded-For")
.validate(true)
.build();
let ctx = RequestContext::new().with_header("X-Forwarded-For", "not-an-ip, 10.0.0.1");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_multi_default_leftmost() {
let extractor = IpExtractor::from_header("X-Forwarded-For");
let ctx = RequestContext::new()
.with_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1, 172.16.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("192.168.1.1".into()))
);
}
#[test]
fn test_ip_trusted_proxies() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".into(), "172.16.0.1".into()],
max_hops: 10,
};
let extractor =
IpExtractor::with_trusted_proxies(vec!["X-Forwarded-For".into()], true, config);
let ctx = RequestContext::new()
.with_header("X-Forwarded-For", "203.0.113.5, 10.0.0.1, 172.16.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("203.0.113.5".into()))
);
}
#[test]
fn test_ip_invalid_returns_none() {
let extractor = IpExtractor::from_header("X-Forwarded-For");
let ctx = RequestContext::new().with_header("X-Forwarded-For", "not-an-ip");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_no_validation() {
let extractor = IpExtractor::builder()
.header_name("X-Forwarded-For")
.validate(false)
.build();
let ctx = RequestContext::new().with_header("X-Forwarded-For", "not-an-ip");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("not-an-ip".into()))
);
}
#[test]
fn test_ip_empty_header() {
let extractor = IpExtractor::from_header("X-Forwarded-For");
let ctx = RequestContext::new().with_header("X-Forwarded-For", "");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_builder() {
let extractor = IpExtractor::builder()
.header_name("X-Real-IP")
.header_name("X-Forwarded-For")
.validate(true)
.build();
let ctx = RequestContext::new().with_header("X-Forwarded-For", "203.0.113.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("203.0.113.1".into()))
);
}
#[test]
fn test_ip_builder_header_names() {
let extractor = IpExtractor::builder()
.header_names(vec!["X-Real-IP", "X-Forwarded-For"])
.validate(true)
.build();
let ctx = RequestContext::new().with_header("X-Forwarded-For", "10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.1".into()))
);
}
#[test]
fn test_ip_builder_with_trusted_proxy_config() {
let config = TrustedProxyConfig {
enabled: true,
proxies: vec!["10.0.0.1".into()],
max_hops: 10,
};
let extractor = IpExtractor::builder()
.header_name("X-Forwarded-For")
.validate(true)
.trusted_proxy_config(config)
.build();
let ctx = RequestContext::new().with_header("X-Forwarded-For", "203.0.113.5, 10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("203.0.113.5".into()))
);
}
#[test]
fn test_mac_name() {
let extractor = MacExtractor::from_header("X-Mac-Address");
assert_eq!(extractor.name(), "MacExtractor");
}
#[test]
fn test_mac_with_dependencies() {
let extractor =
MacExtractor::with_dependencies(Some("X-Mac-Address".into()), Some("mac".into()), true);
let ctx = RequestContext::new().with_header("X-Mac-Address", "AA:BB:CC:DD:EE:FF");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Mac("AA:BB:CC:DD:EE:FF".into()))
);
}
#[test]
fn test_mac_query_param_invalid_value() {
let extractor = MacExtractor::new(None, Some("mac".into()), true);
let ctx = RequestContext::new().with_query_param("mac", "GG:GG:GG:GG:GG:GG");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_mac_query_param_empty_value() {
let extractor = MacExtractor::new(None, Some("mac".into()), true);
let ctx = RequestContext::new().with_query_param("mac", "");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_mac_query_param_not_present() {
let extractor = MacExtractor::new(None, Some("mac".into()), true);
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_mac_query_param_priority() {
let extractor = MacExtractor::new(Some("X-Mac-Address".into()), Some("mac".into()), true);
let ctx = RequestContext::new()
.with_header("X-Mac-Address", "11:22:33:44:55:66")
.with_query_param("mac", "AA:BB:CC:DD:EE:FF");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Mac("11:22:33:44:55:66".into()))
);
}
#[test]
fn test_mac_without_validation() {
let extractor = MacExtractor::new(Some("X-Mac-Address".into()), None, false);
let ctx = RequestContext::new().with_header("X-Mac-Address", "invalid-mac-value");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Mac("invalid-mac-value".into()))
);
}
#[test]
fn test_mac_short_hex() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "AABB");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_mac_from_header() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "00:1A:2B:3C:4D:5E");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Mac("00:1A:2B:3C:4D:5E".into()))
);
}
#[test]
fn test_mac_from_query_param() {
let extractor = MacExtractor::from_query_param("mac");
let ctx = RequestContext::new().with_query_param("mac", "00:1A:2B:3C:4D:5E");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Mac("00:1A:2B:3C:4D:5E".into()))
);
}
#[test]
fn test_mac_colon_format() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "AA:BB:CC:DD:EE:FF");
assert!(extractor.extract(&ctx).is_some());
}
#[test]
fn test_mac_hyphen_format() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "AA-BB-CC-DD-EE-FF");
assert!(extractor.extract(&ctx).is_some());
}
#[test]
fn test_mac_dot_format() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "AABB.CCDD.EEFF");
assert!(extractor.extract(&ctx).is_some());
}
#[test]
fn test_mac_plain_hex() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "AABBCCDDEEFF");
assert!(extractor.extract(&ctx).is_some());
}
#[test]
fn test_mac_invalid() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "GG:1A:2B:3C:4D:5E");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_mac_empty() {
let extractor = MacExtractor::from_header("X-Mac-Address");
let ctx = RequestContext::new().with_header("X-Mac-Address", "");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_mac_builder() {
let extractor = MacExtractor::builder()
.header_name("X-Mac-Address")
.query_param_name("mac_addr")
.validate(true)
.build();
let ctx = RequestContext::new().with_header("X-Mac-Address", "00:1A:2B:3C:4D:5E");
assert!(extractor.extract(&ctx).is_some());
}
#[test]
fn test_api_key_name() {
let extractor = ApiKeyExtractor::from_authorization_header();
assert_eq!(extractor.name(), "ApiKeyExtractor");
}
#[test]
fn test_api_key_with_dependencies() {
let extractor = ApiKeyExtractor::with_dependencies(
Some("Authorization".into()),
None,
Some("Bearer ".into()),
);
let ctx = RequestContext::new().with_header("Authorization", "Bearer token-123");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("token-123".into()))
);
}
#[test]
fn test_api_key_from_authorization() {
let extractor = ApiKeyExtractor::from_authorization_header();
let ctx = RequestContext::new().with_header("Authorization", "Bearer sk-12345");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("sk-12345".into()))
);
}
#[test]
fn test_api_key_from_header() {
let extractor = ApiKeyExtractor::from_header("X-API-Key");
let ctx = RequestContext::new().with_header("X-API-Key", "my-api-key");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("my-api-key".into()))
);
}
#[test]
fn test_api_key_from_query_param() {
let extractor = ApiKeyExtractor::from_query_param("api_key");
let ctx = RequestContext::new().with_query_param("api_key", "key-from-query");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_api_key_bearer_stripping() {
let extractor = ApiKeyExtractor::from_authorization_header();
let ctx = RequestContext::new().with_header("Authorization", "Bearer sk-999 ");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("sk-999".into()))
);
}
#[test]
fn test_api_key_no_prefix() {
let extractor = ApiKeyExtractor::from_header("X-API-Key");
let ctx = RequestContext::new().with_header("X-API-Key", "raw-key-value");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("raw-key-value".into()))
);
}
#[test]
fn test_api_key_empty_after_prefix() {
let extractor = ApiKeyExtractor::from_authorization_header();
let ctx = RequestContext::new().with_header("Authorization", "Bearer ");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_api_key_missing_prefix() {
let extractor = ApiKeyExtractor::from_authorization_header();
let ctx = RequestContext::new().with_header("Authorization", "naked-key");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_api_key_empty_header() {
let extractor = ApiKeyExtractor::from_header("X-API-Key");
let ctx = RequestContext::new().with_header("X-API-Key", "");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_api_key_builder() {
let extractor = ApiKeyExtractor::builder()
.header_name("Authorization")
.prefix("Bearer ")
.build();
let ctx = RequestContext::new().with_header("Authorization", "Bearer builder-key");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("builder-key".into()))
);
}
#[test]
fn test_composite_first_match() {
let extractor = CompositeExtractor::new(
vec![
Box::new(UserIdExtractor::from_header("X-User-Id")),
Box::new(IpExtractor::new_default()),
],
false,
);
let ctx = RequestContext::new()
.with_header("X-User-Id", "user99")
.with_client_ip("10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("user99".into()))
);
}
#[test]
fn test_composite_priority() {
let extractor = CompositeExtractor::new(
vec![
Box::new(UserIdExtractor::from_header("X-User-Id")),
Box::new(ApiKeyExtractor::from_header("X-API-Key")),
],
false,
);
let ctx = RequestContext::new()
.with_header("X-API-Key", "key-val")
.with_client_ip("10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("key-val".into()))
);
}
#[test]
fn test_custom_name() {
let extractor = CustomExtractor::new("MyCustom", |_| None);
assert_eq!(extractor.name(), "MyCustom");
}
#[test]
fn test_custom_closure() {
let extractor = CustomExtractor::new("MyExt", |ctx| {
ctx.get_header("X-Custom")
.map(|v| Identifier::UserId(v.clone()))
});
let ctx = RequestContext::new().with_header("X-Custom", "custom-val");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::UserId("custom-val".into()))
);
}
#[test]
fn test_custom_returns_none() {
let extractor = CustomExtractor::new("Empty", |_| None);
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_device_id_name() {
let extractor = DeviceIdExtractor::from_header("X-Device-Id");
assert_eq!(extractor.name(), "DeviceIdExtractor");
}
#[test]
fn test_device_id_with_dependencies() {
let extractor =
DeviceIdExtractor::with_dependencies(Some("X-Device-Id".into()), Some("did".into()));
let ctx = RequestContext::new().with_header("X-Device-Id", "dep-device");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::DeviceId("dep-device".into()))
);
}
#[test]
fn test_device_id_empty_query_param_fallback() {
let extractor =
DeviceIdExtractor::new(Some("X-Device-Id".into()), Some("device_id".into()));
let ctx = RequestContext::new().with_header("X-Device-Id", "from-header");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::DeviceId("from-header".into()))
);
}
#[test]
fn test_device_id_query_param_empty() {
let extractor = DeviceIdExtractor::new(None, Some("device_id".into()));
let ctx = RequestContext::new().with_query_param("device_id", "");
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_device_id_query_param_not_present() {
let extractor = DeviceIdExtractor::new(None, Some("device_id".into()));
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_device_id_from_header() {
let extractor = DeviceIdExtractor::from_header("X-Device-Id");
let ctx = RequestContext::new().with_header("X-Device-Id", "device-abc");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::DeviceId("device-abc".into()))
);
}
#[test]
fn test_device_id_from_query_param() {
let extractor = DeviceIdExtractor::from_query_param("device_id");
let ctx = RequestContext::new().with_query_param("device_id", "dev-xyz");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::DeviceId("dev-xyz".into()))
);
}
#[test]
fn test_device_id_missing() {
let extractor = DeviceIdExtractor::from_header("X-Device-Id");
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_device_id_builder() {
let extractor = DeviceIdExtractor::builder()
.header_name("X-Device-Id")
.query_param_name("did")
.build();
let ctx = RequestContext::new().with_header("X-Device-Id", "built-device");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::DeviceId("built-device".into()))
);
}
#[test]
fn test_user_id_builder_empty() {
let extractor = UserIdExtractor::builder().build();
let ctx = RequestContext::new();
assert_eq!(extractor.extract(&ctx), None);
}
#[test]
fn test_ip_builder_empty_headers() {
let extractor = IpExtractor::builder().validate(true).build();
let ctx = RequestContext::new().with_client_ip("10.0.0.1");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Ip("10.0.0.1".into()))
);
}
#[test]
fn test_mac_builder_validate_off() {
let extractor = MacExtractor::builder()
.header_name("X-Mac-Address")
.validate(false)
.build();
let ctx = RequestContext::new().with_header("X-Mac-Address", "invalid-mac");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::Mac("invalid-mac".into()))
);
}
#[test]
fn test_api_key_builder_no_prefix() {
let extractor = ApiKeyExtractor::builder().header_name("X-API-Key").build();
let ctx = RequestContext::new().with_header("X-API-Key", "plain-key");
assert_eq!(
extractor.extract(&ctx),
Some(Identifier::ApiKey("plain-key".into()))
);
}
}