Skip to content

Commit 8531ec5

Browse files
authored
Merge pull request #253 from IABTechLab/ian-UID2-6151-add-traffic-filter-class
add traffic filter class
2 parents 213eb5f + 64f1e61 commit 8531ec5

File tree

2 files changed

+596
-0
lines changed

2 files changed

+596
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.uid2.optout.vertx;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.Collections;
9+
import java.io.InputStream;
10+
import java.nio.file.Files;
11+
import java.nio.file.Paths;
12+
import java.nio.charset.StandardCharsets;
13+
import io.vertx.core.json.JsonObject;
14+
import io.vertx.core.json.JsonArray;
15+
16+
public class OptOutTrafficFilter {
17+
private static final Logger LOGGER = LoggerFactory.getLogger(OptOutTrafficFilter.class);
18+
19+
private final String trafficFilterConfigPath;
20+
List<TrafficFilterRule> filterRules;
21+
22+
/**
23+
* Traffic filter rule defining a time range and a list of IP addresses to exclude
24+
*/
25+
private static class TrafficFilterRule {
26+
private final List<Long> range;
27+
private final List<String> ipAddresses;
28+
29+
TrafficFilterRule(List<Long> range, List<String> ipAddresses) {
30+
this.range = range;
31+
this.ipAddresses = ipAddresses;
32+
}
33+
34+
public long getRangeStart() {
35+
return range.get(0);
36+
}
37+
public long getRangeEnd() {
38+
return range.get(1);
39+
}
40+
public List<String> getIpAddresses() {
41+
return ipAddresses;
42+
}
43+
}
44+
45+
public static class MalformedTrafficFilterConfigException extends Exception {
46+
public MalformedTrafficFilterConfigException(String message) {
47+
super(message);
48+
}
49+
}
50+
51+
/**
52+
* Constructor for OptOutTrafficFilter
53+
*
54+
* @param trafficFilterConfigPath S3 path for traffic filter config
55+
* @throws MalformedTrafficFilterConfigException if the traffic filter config is invalid
56+
*/
57+
public OptOutTrafficFilter(String trafficFilterConfigPath) throws MalformedTrafficFilterConfigException {
58+
this.trafficFilterConfigPath = trafficFilterConfigPath;
59+
// Initial filter rules load
60+
this.filterRules = Collections.emptyList(); // start empty
61+
reloadTrafficFilterConfig(); // load ConfigMap
62+
63+
LOGGER.info("OptOutTrafficFilter initialized: filterRules={}",
64+
filterRules.size());
65+
}
66+
67+
/**
68+
* Reload traffic filter config from ConfigMap.
69+
* Expected format:
70+
* {
71+
* "denylist_requests": [
72+
* {range: [startTimestamp, endTimestamp], IPs: ["ip1"]},
73+
* {range: [startTimestamp, endTimestamp], IPs: ["ip1", "ip2"]},
74+
* {range: [startTimestamp, endTimestamp], IPs: ["ip1", "ip3"]},
75+
* ]
76+
* }
77+
*
78+
* Can be called periodically to pick up config changes without restarting.
79+
*/
80+
public void reloadTrafficFilterConfig() throws MalformedTrafficFilterConfigException {
81+
LOGGER.info("Loading traffic filter config from ConfigMap");
82+
try (InputStream is = Files.newInputStream(Paths.get(trafficFilterConfigPath))) {
83+
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
84+
JsonObject filterConfigJson = new JsonObject(content);
85+
86+
this.filterRules = parseFilterRules(filterConfigJson);
87+
88+
LOGGER.info("Successfully loaded traffic filter config from ConfigMap: filterRules={}",
89+
filterRules.size());
90+
91+
} catch (Exception e) {
92+
LOGGER.warn("No traffic filter config found at: {}", trafficFilterConfigPath, e);
93+
throw new MalformedTrafficFilterConfigException(e.getMessage());
94+
}
95+
}
96+
97+
/**
98+
* Parse request filtering rules from JSON config
99+
*/
100+
List<TrafficFilterRule> parseFilterRules(JsonObject config) throws MalformedTrafficFilterConfigException {
101+
List<TrafficFilterRule> rules = new ArrayList<>();
102+
try {
103+
JsonArray denylistRequests = config.getJsonArray("denylist_requests");
104+
if (denylistRequests == null) {
105+
LOGGER.error("Invalid traffic filter config: denylist_requests is null");
106+
throw new MalformedTrafficFilterConfigException("Invalid traffic filter config: denylist_requests is null");
107+
}
108+
for (int i = 0; i < denylistRequests.size(); i++) {
109+
JsonObject ruleJson = denylistRequests.getJsonObject(i);
110+
111+
// parse range
112+
var rangeJson = ruleJson.getJsonArray("range");
113+
List<Long> range = new ArrayList<>();
114+
if (rangeJson != null && rangeJson.size() == 2) {
115+
long start = rangeJson.getLong(0);
116+
long end = rangeJson.getLong(1);
117+
118+
if (start >= end) {
119+
LOGGER.error("Invalid traffic filter rule: range start must be less than end: {}", ruleJson.encode());
120+
throw new MalformedTrafficFilterConfigException("Invalid traffic filter rule: range start must be less than end");
121+
}
122+
range.add(start);
123+
range.add(end);
124+
}
125+
126+
// parse IPs
127+
var ipAddressesJson = ruleJson.getJsonArray("IPs");
128+
List<String> ipAddresses = new ArrayList<>();
129+
if (ipAddressesJson != null) {
130+
for (int j = 0; j < ipAddressesJson.size(); j++) {
131+
ipAddresses.add(ipAddressesJson.getString(j));
132+
}
133+
}
134+
135+
// log error and throw exception if rule is invalid
136+
if (range.size() != 2 || ipAddresses.size() == 0 || range.get(1) - range.get(0) > 86400) { // range must be 24 hours or less
137+
LOGGER.error("Invalid traffic filter rule, range must be 24 hours or less: {}", ruleJson.encode());
138+
throw new MalformedTrafficFilterConfigException("Invalid traffic filter rule, range must be 24 hours or less");
139+
}
140+
141+
TrafficFilterRule rule = new TrafficFilterRule(range, ipAddresses);
142+
143+
LOGGER.info("Loaded traffic filter rule: range=[{}, {}], IPs={}", rule.getRangeStart(), rule.getRangeEnd(), rule.getIpAddresses());
144+
rules.add(rule);
145+
}
146+
return rules;
147+
} catch (Exception e) {
148+
LOGGER.error("Failed to parse traffic filter rules: config={}, error={}", config.encode(), e.getMessage());
149+
throw new MalformedTrafficFilterConfigException(e.getMessage());
150+
}
151+
}
152+
153+
public boolean isDenylisted(SqsParsedMessage message) {
154+
long timestamp = message.getTimestamp();
155+
String clientIp = message.getClientIp();
156+
157+
if (clientIp == null || clientIp.isEmpty()) {
158+
LOGGER.error("Request does not contain client IP, timestamp={}", timestamp);
159+
return false;
160+
}
161+
162+
for (TrafficFilterRule rule : filterRules) {
163+
if(timestamp >= rule.getRangeStart() && timestamp <= rule.getRangeEnd()) {
164+
if(rule.getIpAddresses().contains(clientIp)) {
165+
return true;
166+
}
167+
};
168+
}
169+
return false;
170+
}
171+
172+
}

0 commit comments

Comments
 (0)