Skip to content

Commit 03b2874

Browse files
committed
feat: add integration and pipeline tests for set_filter_state
- Add 3 integration tests for xDS config parsing (Istio waypoint) - Add 5 HTTP pipeline tests for filter application - Fix skip_if_empty to handle '-' default values - Add :authority and :method pseudo-headers to RequestContext Signed-off-by: Eeshu-Yadav <[email protected]>
1 parent 781f711 commit 03b2874

File tree

1 file changed

+352
-1
lines changed

1 file changed

+352
-1
lines changed

orion-lib/src/listeners/http_connection_manager.rs

Lines changed: 352 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ fn apply_set_filter_state_filters<B>(
274274
use orion_configuration::config::network_filters::http_connection_manager::http_filters::set_filter_state::SharedWithUpstream as ConfigSharedWithUpstream;
275275
use crate::SharedWithUpstream as RuntimeSharedWithUpstream;
276276

277+
tracing::trace!(
278+
"Applying set_filter_state filters: method={}, uri={}, filters={}",
279+
request.method(),
280+
request.uri(),
281+
http_filters.len()
282+
);
283+
277284
// Build request context from HTTP request
278285
let mut ctx = RequestContext::new();
279286

@@ -284,9 +291,19 @@ fn apply_set_filter_state_filters<B>(
284291
}
285292
}
286293

294+
if ctx.get_header(":authority").is_none() {
295+
if let Some(host) = ctx.get_header("host") {
296+
let host = host.clone();
297+
ctx = ctx.with_header(":authority", host);
298+
}
299+
}
300+
287301
// Extract method
288302
ctx = ctx.with_method(request.method().as_str());
289303

304+
// Also add :method pseudo-header for compatibility with %REQ(:method)%
305+
ctx = ctx.with_header(":method", request.method().as_str());
306+
290307
// Extract path
291308
ctx = ctx.with_path(request.uri().path());
292309

@@ -308,6 +325,12 @@ fn apply_set_filter_state_filters<B>(
308325

309326
// Check if this is a SetFilterState filter
310327
if let Some(HttpFilterValue::SetFilterState(sfs_config)) = &filter.filter {
328+
tracing::debug!(
329+
"Processing SetFilterState filter '{}' with {} entries",
330+
filter.name,
331+
sfs_config.on_request_headers.len()
332+
);
333+
311334
// Process each on_request_headers entry
312335
for value_config in &sfs_config.on_request_headers {
313336
// 1. Evaluate format string
@@ -325,7 +348,7 @@ fn apply_set_filter_state_filters<B>(
325348
};
326349

327350
// 2. Check skip_if_empty
328-
if value_config.skip_if_empty && evaluated_value.is_empty() {
351+
if value_config.skip_if_empty && (evaluated_value.is_empty() || evaluated_value == "-") {
329352
tracing::debug!(
330353
"Skipping filter state key '{}' due to empty value",
331354
value_config.object_key
@@ -1311,6 +1334,10 @@ fn apply_authorization_rules<B>(rbac: &HttpRbac, req: &Request<B>) -> FilterDeci
13111334
#[cfg(test)]
13121335
mod tests {
13131336
use orion_configuration::config::network_filters::http_connection_manager::MatchHost;
1337+
use hyper::Method;
1338+
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
1339+
use crate::filter_state::SharedWithUpstream as RuntimeSharedWithUpstream;
1340+
use crate::listeners::filter_state::{DownstreamConnectionMetadata, DownstreamMetadata};
13141341

13151342
use super::*;
13161343

@@ -1349,4 +1376,328 @@ mod tests {
13491376
let request = Request::builder().header("host", "domain2.com").body(()).unwrap();
13501377
assert_eq!(select_virtual_host(&request, &[vh1.clone(), vh2.clone(), vh3.clone()]), None);
13511378
}
1379+
1380+
#[test]
1381+
fn test_apply_set_filter_state_filters_basic() {
1382+
use orion_configuration::config::network_filters::http_connection_manager::http_filters::{
1383+
set_filter_state::{FilterStateValue, FormatString, SharedWithUpstream as ConfigSharedWithUpstream},
1384+
};
1385+
use compact_str::CompactString;
1386+
1387+
let filter_state = FilterState::new();
1388+
let format_evaluator = FormatStringEvaluator::new();
1389+
1390+
let sfs_config = orion_configuration::config::network_filters::http_connection_manager::http_filters::set_filter_state::SetFilterState {
1391+
on_request_headers: vec![
1392+
FilterStateValue {
1393+
object_key: CompactString::from("io.istio.connect_authority"),
1394+
format_string: FormatString::Text(CompactString::from("%REQ(:authority)%")),
1395+
factory_key: None,
1396+
read_only: true,
1397+
shared_with_upstream: ConfigSharedWithUpstream::Once,
1398+
skip_if_empty: false,
1399+
},
1400+
],
1401+
};
1402+
1403+
let http_filter = Arc::new(HttpFilter {
1404+
name: CompactString::from("test.set_filter_state"),
1405+
disabled: false,
1406+
filter: Some(HttpFilterValue::SetFilterState(sfs_config)),
1407+
});
1408+
1409+
let request = Request::builder()
1410+
.method(Method::CONNECT)
1411+
.uri("example.com:443")
1412+
.header("host", "example.com:443")
1413+
.body(())
1414+
.unwrap();
1415+
1416+
let downstream_metadata = DownstreamMetadata::new(
1417+
DownstreamConnectionMetadata::Socket {
1418+
peer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 54321),
1419+
local_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)), 15008),
1420+
original_destination_address: None,
1421+
},
1422+
None::<String>,
1423+
);
1424+
1425+
apply_set_filter_state_filters(
1426+
&request,
1427+
&downstream_metadata,
1428+
&[http_filter],
1429+
&filter_state,
1430+
&format_evaluator,
1431+
);
1432+
1433+
let (value, read_only, shared_with_upstream) = filter_state.get_with_metadata("io.istio.connect_authority").unwrap();
1434+
assert_eq!(value.as_str(), "example.com:443");
1435+
assert!(read_only);
1436+
assert!(matches!(shared_with_upstream, RuntimeSharedWithUpstream::Once));
1437+
}
1438+
1439+
#[test]
1440+
fn test_apply_set_filter_state_filters_skip_if_empty() {
1441+
use orion_configuration::config::network_filters::http_connection_manager::http_filters::{
1442+
set_filter_state::{FilterStateValue, FormatString, SharedWithUpstream as ConfigSharedWithUpstream},
1443+
};
1444+
use compact_str::CompactString;
1445+
1446+
let filter_state = FilterState::new();
1447+
let format_evaluator = FormatStringEvaluator::new();
1448+
1449+
// Create config with skip_if_empty=true for a missing header
1450+
let sfs_config = orion_configuration::config::network_filters::http_connection_manager::http_filters::set_filter_state::SetFilterState {
1451+
on_request_headers: vec![
1452+
FilterStateValue {
1453+
object_key: CompactString::from("test.missing_header"),
1454+
format_string: FormatString::Text(CompactString::from("%REQ(x-missing-header)%")),
1455+
factory_key: None,
1456+
read_only: false,
1457+
shared_with_upstream: ConfigSharedWithUpstream::None,
1458+
skip_if_empty: true,
1459+
},
1460+
],
1461+
};
1462+
1463+
let http_filter = Arc::new(HttpFilter {
1464+
name: CompactString::from("test.set_filter_state"),
1465+
disabled: false,
1466+
filter: Some(HttpFilterValue::SetFilterState(sfs_config)),
1467+
});
1468+
1469+
let request = Request::builder()
1470+
.method(Method::GET)
1471+
.uri("/test")
1472+
.body(())
1473+
.unwrap();
1474+
1475+
let downstream_metadata = DownstreamMetadata::new(
1476+
DownstreamConnectionMetadata::Socket {
1477+
peer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
1478+
local_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
1479+
original_destination_address: None,
1480+
},
1481+
None::<String>,
1482+
);
1483+
1484+
apply_set_filter_state_filters(
1485+
&request,
1486+
&downstream_metadata,
1487+
&[http_filter],
1488+
&filter_state,
1489+
&format_evaluator,
1490+
);
1491+
1492+
assert!(filter_state.get("test.missing_header").is_err());
1493+
}
1494+
1495+
#[test]
1496+
fn test_apply_set_filter_state_filters_client_address() {
1497+
use orion_configuration::config::network_filters::http_connection_manager::http_filters::{
1498+
set_filter_state::{FilterStateValue, FormatString, SharedWithUpstream as ConfigSharedWithUpstream},
1499+
};
1500+
use compact_str::CompactString;
1501+
1502+
let filter_state = FilterState::new();
1503+
let format_evaluator = FormatStringEvaluator::new();
1504+
1505+
// Create config for client address
1506+
let sfs_config = orion_configuration::config::network_filters::http_connection_manager::http_filters::set_filter_state::SetFilterState {
1507+
on_request_headers: vec![
1508+
FilterStateValue {
1509+
object_key: CompactString::from("io.istio.client_address"),
1510+
format_string: FormatString::Text(CompactString::from("%DOWNSTREAM_REMOTE_ADDRESS%")),
1511+
factory_key: None,
1512+
read_only: false,
1513+
shared_with_upstream: ConfigSharedWithUpstream::Transitive,
1514+
skip_if_empty: false,
1515+
},
1516+
],
1517+
};
1518+
1519+
let http_filter = Arc::new(HttpFilter {
1520+
name: CompactString::from("test.set_filter_state"),
1521+
disabled: false,
1522+
filter: Some(HttpFilterValue::SetFilterState(sfs_config)),
1523+
});
1524+
1525+
let request = Request::builder()
1526+
.method(Method::GET)
1527+
.uri("/")
1528+
.body(())
1529+
.unwrap();
1530+
1531+
let downstream_metadata = DownstreamMetadata::new(
1532+
DownstreamConnectionMetadata::Socket {
1533+
peer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 45678),
1534+
local_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8080),
1535+
original_destination_address: None,
1536+
},
1537+
None::<String>,
1538+
);
1539+
1540+
// Apply filters
1541+
apply_set_filter_state_filters(
1542+
&request,
1543+
&downstream_metadata,
1544+
&[http_filter],
1545+
&filter_state,
1546+
&format_evaluator,
1547+
);
1548+
1549+
// Verify the client address was captured
1550+
let (value, read_only, shared_with_upstream) = filter_state.get_with_metadata("io.istio.client_address").unwrap();
1551+
assert_eq!(value.as_str(), "192.168.1.100:45678");
1552+
assert!(!read_only);
1553+
assert!(matches!(shared_with_upstream, RuntimeSharedWithUpstream::Transitive));
1554+
}
1555+
1556+
#[test]
1557+
fn test_apply_set_filter_state_filters_multiple_entries() {
1558+
use orion_configuration::config::network_filters::http_connection_manager::http_filters::{
1559+
set_filter_state::{FilterStateValue, FormatString, SharedWithUpstream as ConfigSharedWithUpstream},
1560+
};
1561+
use compact_str::CompactString;
1562+
1563+
let filter_state = FilterState::new();
1564+
let format_evaluator = FormatStringEvaluator::new();
1565+
1566+
// Create config with multiple filter state entries
1567+
let sfs_config = orion_configuration::config::network_filters::http_connection_manager::http_filters::set_filter_state::SetFilterState {
1568+
on_request_headers: vec![
1569+
FilterStateValue {
1570+
object_key: CompactString::from("io.istio.connect_authority"),
1571+
format_string: FormatString::Text(CompactString::from("%REQ(:authority)%")),
1572+
factory_key: None,
1573+
read_only: true,
1574+
shared_with_upstream: ConfigSharedWithUpstream::Once,
1575+
skip_if_empty: false,
1576+
},
1577+
FilterStateValue {
1578+
object_key: CompactString::from("io.istio.client_address"),
1579+
format_string: FormatString::Text(CompactString::from("%DOWNSTREAM_REMOTE_ADDRESS%")),
1580+
factory_key: None,
1581+
read_only: false,
1582+
shared_with_upstream: ConfigSharedWithUpstream::Transitive,
1583+
skip_if_empty: false,
1584+
},
1585+
FilterStateValue {
1586+
object_key: CompactString::from("test.method"),
1587+
format_string: FormatString::Text(CompactString::from("%REQ(:method)%")),
1588+
factory_key: None,
1589+
read_only: false,
1590+
shared_with_upstream: ConfigSharedWithUpstream::None,
1591+
skip_if_empty: false,
1592+
},
1593+
],
1594+
};
1595+
1596+
let http_filter = Arc::new(HttpFilter {
1597+
name: CompactString::from("test.set_filter_state"),
1598+
disabled: false,
1599+
filter: Some(HttpFilterValue::SetFilterState(sfs_config)),
1600+
});
1601+
1602+
let request = Request::builder()
1603+
.method(Method::CONNECT)
1604+
.uri("dest.example.com:443")
1605+
.header("host", "dest.example.com:443")
1606+
.body(())
1607+
.unwrap();
1608+
1609+
let downstream_metadata = DownstreamMetadata::new(
1610+
DownstreamConnectionMetadata::Socket {
1611+
peer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3)), 12345),
1612+
local_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 31, 0, 1)), 15008),
1613+
original_destination_address: None,
1614+
},
1615+
None::<String>,
1616+
);
1617+
1618+
// Apply filters
1619+
apply_set_filter_state_filters(
1620+
&request,
1621+
&downstream_metadata,
1622+
&[http_filter],
1623+
&filter_state,
1624+
&format_evaluator,
1625+
);
1626+
1627+
// Verify all three entries were set
1628+
assert_eq!(filter_state.get("io.istio.connect_authority").unwrap().as_str(), "dest.example.com:443");
1629+
assert_eq!(filter_state.get("io.istio.client_address").unwrap().as_str(), "10.1.2.3:12345");
1630+
assert_eq!(filter_state.get("test.method").unwrap().as_str(), "CONNECT");
1631+
1632+
// Verify metadata
1633+
let (_, read_only1, shared1) = filter_state.get_with_metadata("io.istio.connect_authority").unwrap();
1634+
assert!(read_only1);
1635+
assert!(matches!(shared1, RuntimeSharedWithUpstream::Once));
1636+
1637+
let (_, read_only2, shared2) = filter_state.get_with_metadata("io.istio.client_address").unwrap();
1638+
assert!(!read_only2);
1639+
assert!(matches!(shared2, RuntimeSharedWithUpstream::Transitive));
1640+
1641+
let (_, read_only3, shared3) = filter_state.get_with_metadata("test.method").unwrap();
1642+
assert!(!read_only3);
1643+
assert!(matches!(shared3, RuntimeSharedWithUpstream::None));
1644+
}
1645+
1646+
#[test]
1647+
fn test_apply_set_filter_state_filters_disabled_filter() {
1648+
use orion_configuration::config::network_filters::http_connection_manager::http_filters::{
1649+
set_filter_state::{FilterStateValue, FormatString, SharedWithUpstream as ConfigSharedWithUpstream},
1650+
};
1651+
use compact_str::CompactString;
1652+
1653+
let filter_state = FilterState::new();
1654+
let format_evaluator = FormatStringEvaluator::new();
1655+
1656+
let sfs_config = orion_configuration::config::network_filters::http_connection_manager::http_filters::set_filter_state::SetFilterState {
1657+
on_request_headers: vec![
1658+
FilterStateValue {
1659+
object_key: CompactString::from("test.disabled"),
1660+
format_string: FormatString::Text(CompactString::from("value")),
1661+
factory_key: None,
1662+
read_only: false,
1663+
shared_with_upstream: ConfigSharedWithUpstream::None,
1664+
skip_if_empty: false,
1665+
},
1666+
],
1667+
};
1668+
1669+
// Create a disabled filter
1670+
let http_filter = Arc::new(HttpFilter {
1671+
name: CompactString::from("test.set_filter_state"),
1672+
disabled: true, // Disabled!
1673+
filter: Some(HttpFilterValue::SetFilterState(sfs_config)),
1674+
});
1675+
1676+
let request = Request::builder()
1677+
.method(Method::GET)
1678+
.uri("/")
1679+
.body(())
1680+
.unwrap();
1681+
1682+
let downstream_metadata = DownstreamMetadata::new(
1683+
DownstreamConnectionMetadata::Socket {
1684+
peer_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12345),
1685+
local_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080),
1686+
original_destination_address: None,
1687+
},
1688+
None::<String>,
1689+
);
1690+
1691+
// Apply filters
1692+
apply_set_filter_state_filters(
1693+
&request,
1694+
&downstream_metadata,
1695+
&[http_filter],
1696+
&filter_state,
1697+
&format_evaluator,
1698+
);
1699+
1700+
// Verify nothing was set (filter was disabled)
1701+
assert!(filter_state.get("test.disabled").is_err());
1702+
}
13521703
}

0 commit comments

Comments
 (0)