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