Skip to content

Commit ff4cd12

Browse files
apaegsRickank
andauthored
Feature/27 ipfilter (#70)
* Add IpFilter and corresponding test skeleton Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Extend IpFilter with blocklist mode and add unit tests Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Enhance IpFilter to return 403 for blocked IPs and add corresponding test case Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Extend IpFilter with allowlist mode and add corresponding unit tests Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Refactor IpFilter to support both allowlist and blocklist modes, update logic, and add unit tests for allowlist mode Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Handle missing client IP in IpFilter, return 400 response, and add corresponding test case Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Refactor tests in `IpFilterTest` to use `assertAll` for improved assertion grouping and readability Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Refactor `IpFilter` to use `HttpResponse` instead of `HttpResponseBuilder` and update tests accordingly Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Add unit tests for edge cases in `IpFilter` allowlist and blocklist modes Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Refactor `IpFilter` and tests to use `HttpResponseBuilder` instead of `HttpResponse` Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Handle empty client IP in `IpFilter`, return 400 response, and add corresponding test case Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Add comments to `IpFilter` empty methods, clarifying no action is required Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Fix typos in comments within `IpFilterTest` Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Add Javadoc comments to `IpFilter` and `IpFilterTest` for improved clarity and documentation * Refactor `IpFilter` to use thread-safe collections and normalize IP addresses * Make `mode` field in `IpFilter` volatile to ensure thread safety * Ensure UTF-8 encoding for response string in `IpFilterTest` and add attribute management to `HttpRequest` * Ensure UTF-8 encoding for response string in `IpFilterTest` and add attribute management to `HttpRequest` Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Integrate IP filtering into `ConnectionHandler` and update configuration to support filter settings Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Refactor IP filter check in `ConnectionHandler` to use `Boolean.TRUE.equals` for improved null safety Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * Validate null inputs for allowed/blocked IPs in `IpFilter`, enhance test coverage, and fix typographical error in comments Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> * refactor: extract applyFilters() method using FilterChainImpl Co-authored-by: Andreas Kaiberger <andreas.kaiberger@gmail.com> * refactor: cache filter list at construction time Co-authored-by: Andreas Kaiberger <andreas.kaiberger@gmail.com> * refactor: cache filter list at construction time Co-authored-by: Andreas Kaiberger <andreas.kaiberger@gmail.com> * test: verify GPG signing * Replace hardcoded status codes in `IpFilter` and `ConnectionHandler` with constants from `HttpResponseBuilder` for better maintainability Co-authored-by: Rickard Ankar <rickard.ankar@iths.se> --------- Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>
1 parent e72f073 commit ff4cd12

7 files changed

Lines changed: 438 additions & 5 deletions

File tree

src/main/java/org/example/ConnectionHandler.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
package org.example;
22

3+
import org.example.config.AppConfig;
4+
import org.example.filter.IpFilter;
35
import org.example.httpparser.HttpParser;
6+
import org.example.httpparser.HttpRequest;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import org.example.filter.Filter;
10+
import org.example.filter.FilterChainImpl;
11+
import org.example.http.HttpResponseBuilder;
12+
import org.example.config.ConfigLoader;
413

514
import java.io.IOException;
615
import java.net.Socket;
@@ -9,21 +18,66 @@ public class ConnectionHandler implements AutoCloseable {
918

1019
Socket client;
1120
String uri;
21+
private final List<Filter> filters;
1222

1323
public ConnectionHandler(Socket client) {
1424
this.client = client;
25+
this.filters = buildFilters();
1526
}
27+
private List<Filter> buildFilters() {
28+
List<Filter> list = new ArrayList<>();
29+
AppConfig config = ConfigLoader.get();
30+
AppConfig.IpFilterConfig ipFilterConfig = config.ipFilter();
31+
if (Boolean.TRUE.equals(ipFilterConfig.enabled())) {
32+
list.add(createIpFilterFromConfig(ipFilterConfig));
33+
}
34+
// Add more filters here...
35+
return list;
36+
}
37+
1638

1739
public void runConnectionHandler() throws IOException {
18-
StaticFileHandler sfh = new StaticFileHandler();
1940
HttpParser parser = new HttpParser();
2041
parser.setReader(client.getInputStream());
2142
parser.parseRequest();
2243
parser.parseHttp();
44+
45+
HttpRequest request = new HttpRequest(
46+
parser.getMethod(),
47+
parser.getUri(),
48+
parser.getVersion(),
49+
parser.getHeadersMap(),
50+
""
51+
);
52+
53+
String clientIp = client.getInetAddress().getHostAddress();
54+
request.setAttribute("clientIp", clientIp);
55+
56+
HttpResponseBuilder response = applyFilters(request);
57+
58+
int statusCode = response.getStatusCode();
59+
if (statusCode == HttpResponseBuilder.SC_FORBIDDEN ||
60+
statusCode == HttpResponseBuilder.SC_BAD_REQUEST) {
61+
byte[] responseBytes = response.build();
62+
client.getOutputStream().write(responseBytes);
63+
client.getOutputStream().flush();
64+
return;
65+
}
66+
2367
resolveTargetFile(parser.getUri());
68+
StaticFileHandler sfh = new StaticFileHandler();
2469
sfh.sendGetRequest(client.getOutputStream(), uri);
2570
}
2671

72+
private HttpResponseBuilder applyFilters(HttpRequest request) {
73+
HttpResponseBuilder response = new HttpResponseBuilder();
74+
75+
FilterChainImpl chain = new FilterChainImpl(filters);
76+
chain.doFilter(request, response);
77+
78+
return response;
79+
}
80+
2781
private void resolveTargetFile(String uri) {
2882
if (uri.matches("/$")) { //matches(/)
2983
this.uri = "index.html";
@@ -39,4 +93,28 @@ private void resolveTargetFile(String uri) {
3993
public void close() throws Exception {
4094
client.close();
4195
}
96+
97+
private IpFilter createIpFilterFromConfig(AppConfig.IpFilterConfig config) {
98+
IpFilter filter = new IpFilter();
99+
100+
// Set mode
101+
if ("ALLOWLIST".equalsIgnoreCase(config.mode())) {
102+
filter.setMode(IpFilter.FilterMode.ALLOWLIST);
103+
} else {
104+
filter.setMode(IpFilter.FilterMode.BLOCKLIST);
105+
}
106+
107+
// Add blocked IPs
108+
for (String ip : config.blockedIps()) {
109+
filter.addBlockedIp(ip);
110+
}
111+
112+
// Add allowed IPs
113+
for (String ip : config.allowedIps()) {
114+
filter.addAllowedIp(ip);
115+
}
116+
117+
filter.init();
118+
return filter;
119+
}
42120
}

src/main/java/org/example/config/AppConfig.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
@JsonIgnoreProperties(ignoreUnknown = true)
77
public record AppConfig(
88
@JsonProperty("server") ServerConfig server,
9-
@JsonProperty("logging") LoggingConfig logging
9+
@JsonProperty("logging") LoggingConfig logging,
10+
@JsonProperty("ipFilter") IpFilterConfig ipFilter
1011
) {
1112
public static AppConfig defaults() {
12-
return new AppConfig(ServerConfig.defaults(), LoggingConfig.defaults());
13+
return new AppConfig(
14+
ServerConfig.defaults(),
15+
LoggingConfig.defaults(),
16+
IpFilterConfig.defaults()
17+
);
1318
}
1419

1520
public AppConfig withDefaultsApplied() {
1621
ServerConfig serverConfig = (server == null ? ServerConfig.defaults() : server.withDefaultsApplied());
1722
LoggingConfig loggingConfig = (logging == null ? LoggingConfig.defaults() : logging.withDefaultsApplied());
18-
return new AppConfig(serverConfig, loggingConfig);
23+
IpFilterConfig ipFilterConfig = (ipFilter == null ? IpFilterConfig.defaults() : ipFilter.withDefaultsApplied()); // ← LÄGG TILL
24+
return new AppConfig(serverConfig, loggingConfig, ipFilterConfig); // ← UPPDATERA DENNA RAD
1925
}
2026

2127
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -50,4 +56,24 @@ public LoggingConfig withDefaultsApplied() {
5056
return new LoggingConfig(lvl);
5157
}
5258
}
59+
60+
@JsonIgnoreProperties(ignoreUnknown = true)
61+
public record IpFilterConfig(
62+
@JsonProperty("enabled") Boolean enabled,
63+
@JsonProperty("mode") String mode,
64+
@JsonProperty("blockedIps") java.util.List<String> blockedIps,
65+
@JsonProperty("allowedIps") java.util.List<String> allowedIps
66+
) {
67+
public static IpFilterConfig defaults() {
68+
return new IpFilterConfig(false, "BLOCKLIST", java.util.List.of(), java.util.List.of());
69+
}
70+
71+
public IpFilterConfig withDefaultsApplied() {
72+
Boolean e = (enabled == null) ? false : enabled;
73+
String m = (mode == null || mode.isBlank()) ? "BLOCKLIST" : mode;
74+
java.util.List<String> blocked = (blockedIps == null) ? java.util.List.of() : blockedIps;
75+
java.util.List<String> allowed = (allowedIps == null) ? java.util.List.of() : allowedIps;
76+
return new IpFilterConfig(e, m, blocked, allowed);
77+
}
78+
}
5379
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.example.filter;
2+
3+
4+
import org.example.http.HttpResponseBuilder;
5+
import org.example.httpparser.HttpRequest;
6+
7+
import java.util.Set;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
10+
/**
11+
* A filter that allows or blocks HTTP requests based on the client's IP address.
12+
* The filter supports two modes:
13+
* ALLOWLIST – only IP addresses in the allowlist are permitted
14+
* BLOCKLIST – all IP addresses are permitted except those in the blocklist
15+
*/
16+
public class IpFilter implements Filter {
17+
18+
private final Set<String> blockedIps = ConcurrentHashMap.newKeySet();
19+
private final Set<String> allowedIps = ConcurrentHashMap.newKeySet();
20+
private volatile FilterMode mode = FilterMode.BLOCKLIST;
21+
22+
/**
23+
* Defines the filtering mode.
24+
*/
25+
public enum FilterMode {
26+
ALLOWLIST,
27+
BLOCKLIST
28+
}
29+
30+
@Override
31+
public void init() {
32+
// Intentionally empty - no initialization needed
33+
}
34+
35+
/**
36+
* Filters incoming HTTP requests based on the client's IP address.
37+
*
38+
* @param request the incoming HTTP request
39+
* @param response the HTTP response builder used when blocking requests
40+
* @param chain the filter chain to continue if the request is allowed
41+
*/
42+
@Override
43+
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
44+
String clientIp = normalizeIp((String) request.getAttribute("clientIp"));
45+
46+
if (clientIp == null || clientIp.trim().isEmpty()) {
47+
response.setStatusCode(HttpResponseBuilder.SC_BAD_REQUEST);
48+
response.setBody("Bad Request: Missing client IP address");
49+
return;
50+
}
51+
52+
boolean allowed = isIpAllowed(clientIp);
53+
54+
if (allowed) {
55+
chain.doFilter(request, response);
56+
} else {
57+
response.setStatusCode(HttpResponseBuilder.SC_FORBIDDEN);
58+
response.setBody("Forbidden: IP address " + clientIp + " is not allowed");
59+
}
60+
}
61+
62+
@Override
63+
public void destroy() {
64+
// Intentionally empty - no cleanup needed
65+
}
66+
67+
/**
68+
* Determines whether an IP address is allowed based on the current filter mode.
69+
*
70+
* @param ip the IP address to check
71+
* @return true if the IP address is allowed, otherwise false
72+
*/
73+
private boolean isIpAllowed(String ip) {
74+
if (mode == FilterMode.ALLOWLIST) {
75+
return allowedIps.contains(ip);
76+
} else {
77+
return !blockedIps.contains(ip);
78+
}
79+
}
80+
81+
/**
82+
* Trims leading and trailing whitespace from an IP address.
83+
*
84+
* @param ip the IP address
85+
* @return the trimmed IP address, or {@code null} if the input is {@code null}
86+
*/
87+
private String normalizeIp(String ip) {
88+
return ip == null ? null : ip.trim();
89+
}
90+
91+
public void setMode(FilterMode mode) {
92+
this.mode = mode;
93+
}
94+
95+
public void addBlockedIp(String ip) {
96+
if (ip == null) {
97+
throw new IllegalArgumentException("IP address cannot be null");
98+
}
99+
blockedIps.add(normalizeIp(ip));
100+
}
101+
102+
public void addAllowedIp(String ip) {
103+
if (ip == null) {
104+
throw new IllegalArgumentException("IP address cannot be null");
105+
}
106+
allowedIps.add(normalizeIp(ip));
107+
}
108+
}

src/main/java/org/example/http/HttpResponseBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public void setStatusCode(int statusCode) {
6565
this.statusCode = statusCode;
6666
}
6767

68+
public int getStatusCode() {
69+
return this.statusCode;
70+
}
71+
6872
public void setBody(String body) {
6973
this.body = body != null ? body : "";
7074
this.bytebody = null;

src/main/java/org/example/httpparser/HttpRequest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.example.httpparser;
22

33
import java.util.Collections;
4+
import java.util.HashMap;
45
import java.util.Map;
56

67
/*
@@ -15,6 +16,7 @@ public class HttpRequest {
1516
private final String version;
1617
private final Map<String, String> headers;
1718
private final String body;
19+
private final Map<String, Object> attributes = new HashMap<>();
1820

1921
public HttpRequest(String method,
2022
String path,
@@ -38,4 +40,10 @@ public Map<String, String> getHeaders() {
3840
return headers; }
3941
public String getBody() {
4042
return body; }
43+
public void setAttribute(String key, Object value) {
44+
attributes.put(key, value);
45+
}
46+
public Object getAttribute(String key) {
47+
return attributes.get(key);
48+
}
4149
}

src/main/resources/application.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,10 @@ server:
33
rootDir: ./www
44

55
logging:
6-
level: INFO
6+
level: INFO
7+
8+
ipFilter:
9+
enabled: false
10+
mode: "BLOCKLIST"
11+
blockedIps: [ ]
12+
allowedIps: [ ]

0 commit comments

Comments
 (0)